@foldkit/oxlint-plugin 0.13.0-canary.41f95ced5e07 → 0.13.0-canary.4cb3546e35b7

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/all.json CHANGED
@@ -17,6 +17,7 @@
17
17
  "foldkit/mount-factory-must-use-element": "error",
18
18
  "foldkit/no-array-index-view-keys": "error",
19
19
  "foldkit/no-child-message-construction-in-root": "error",
20
+ "foldkit/no-direct-submodel-state-update": "error",
20
21
  "foldkit/no-disabling-dev-guardrails": "error",
21
22
  "foldkit/no-duplicate-onmount-per-element": "error",
22
23
  "foldkit/no-empty-children-array": "error",
@@ -38,6 +39,7 @@
38
39
  "foldkit/prefer-command-mapmessage": "error",
39
40
  "foldkit/prefer-effect-module-names": "error",
40
41
  "foldkit/prefer-option-over-nullable-in-model": "error",
42
+ "foldkit/require-fold-for-child-update-result": "error",
41
43
  "foldkit/require-rel-for-external-link": "error",
42
44
  "foldkit/selection-submodel-factory-at-module-scope": "error",
43
45
  "foldkit/wrap-child-output-in-got-message": "error"
@@ -100,6 +102,7 @@
100
102
  "foldkit/mount-factory-must-use-element": "off",
101
103
  "foldkit/no-array-index-view-keys": "off",
102
104
  "foldkit/no-child-message-construction-in-root": "off",
105
+ "foldkit/no-direct-submodel-state-update": "off",
103
106
  "foldkit/no-disabling-dev-guardrails": "off",
104
107
  "foldkit/no-duplicate-onmount-per-element": "off",
105
108
  "foldkit/no-empty-children-array": "off",
@@ -119,6 +122,7 @@
119
122
  "foldkit/prefer-callable-message-constructor": "off",
120
123
  "foldkit/prefer-command-mapmessage": "off",
121
124
  "foldkit/prefer-option-over-nullable-in-model": "off",
125
+ "foldkit/require-fold-for-child-update-result": "off",
122
126
  "foldkit/require-rel-for-external-link": "off",
123
127
  "foldkit/selection-submodel-factory-at-module-scope": "off",
124
128
  "foldkit/wrap-child-output-in-got-message": "off"
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ declare const _default: {
23
23
  readonly 'mount-factory-must-use-element': import("effect-oxlint").CreateRule;
24
24
  readonly 'no-array-index-view-keys': import("effect-oxlint").CreateRule;
25
25
  readonly 'no-child-message-construction-in-root': import("effect-oxlint").CreateRule;
26
+ readonly 'no-direct-submodel-state-update': import("effect-oxlint").CreateRule;
26
27
  readonly 'no-disabling-dev-guardrails': import("effect-oxlint").CreateRule;
27
28
  readonly 'no-duplicate-onmount-per-element': import("effect-oxlint").CreateRule;
28
29
  readonly 'no-empty-children-array': import("effect-oxlint").CreateRule;
@@ -44,6 +45,7 @@ declare const _default: {
44
45
  readonly 'prefer-command-mapmessage': import("effect-oxlint").CreateRule;
45
46
  readonly 'prefer-effect-module-names': import("effect-oxlint").CreateRule;
46
47
  readonly 'prefer-option-over-nullable-in-model': import("effect-oxlint").CreateRule;
48
+ readonly 'require-fold-for-child-update-result': import("effect-oxlint").CreateRule;
47
49
  readonly 'require-rel-for-external-link': import("effect-oxlint").CreateRule;
48
50
  readonly 'selection-submodel-factory-at-module-scope': import("effect-oxlint").CreateRule;
49
51
  readonly 'wrap-child-output-in-got-message': import("effect-oxlint").CreateRule;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAkFtC,KAAK,QAAQ,GAAG,QAAQ,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACpB,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,CAAA;CAC3C,CAAC,CAAA;AAEF,KAAK,gBAAgB,GAAG,MAAM,CAAC,YAAY,GAAG;IAC5C,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;CAC3B,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6EG,WAAW;QACX,GAAG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAsFtC,KAAK,QAAQ,GAAG,QAAQ,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACpB,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,CAAA;CAC3C,CAAC,CAAA;AAEF,KAAK,gBAAgB,GAAG,MAAM,CAAC,YAAY,GAAG;IAC5C,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;CAC3B,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6EG,WAAW;QACX,GAAG"}
package/dist/index.js CHANGED
@@ -535,7 +535,7 @@ function isBoolean(input) {
535
535
  * @category guards
536
536
  * @since 2.0.0
537
537
  */
538
- function isFunction$2(input) {
538
+ function isFunction$3(input) {
539
539
  return typeof input === "function";
540
540
  }
541
541
  /**
@@ -648,7 +648,7 @@ function isObject(input) {
648
648
  * @since 4.0.0
649
649
  */
650
650
  function isObjectKeyword(input) {
651
- return typeof input === "object" && input !== null || isFunction$2(input);
651
+ return typeof input === "object" && input !== null || isFunction$3(input);
652
652
  }
653
653
  /**
654
654
  * Checks whether a value has a given property key.
@@ -1738,7 +1738,7 @@ const NodeInspectSymbol = /*#__PURE__*/ Symbol.for("nodejs.util.inspect.custom")
1738
1738
  const toJson = (input) => {
1739
1739
  try {
1740
1740
  input = redact(input);
1741
- if (hasProperty(input, "toJSON") && isFunction$2(input["toJSON"]) && input["toJSON"].length === 0) return input.toJSON();
1741
+ if (hasProperty(input, "toJSON") && isFunction$3(input["toJSON"]) && input["toJSON"].length === 0) return input.toJSON();
1742
1742
  else if (Array.isArray(input)) return input.map(toJson);
1743
1743
  return input;
1744
1744
  } catch {
@@ -2825,6 +2825,62 @@ const map$5 = /*#__PURE__*/ dual(2, (self, f) => isNone(self) ? none() : some$1(
2825
2825
  */
2826
2826
  const flatMap$3 = /*#__PURE__*/ dual(2, (self, f) => isNone(self) ? none() : f(self.value));
2827
2827
  /**
2828
+ * Combines a structure of `Option`s (tuple, struct, or iterable) into a single
2829
+ * `Option` containing the unwrapped structure.
2830
+ *
2831
+ * **When to use**
2832
+ *
2833
+ * Use when you need to combine multiple `Option` values into one while
2834
+ * preserving the input shape, with any `None` making the result `None`.
2835
+ *
2836
+ * **Details**
2837
+ *
2838
+ * - Tuple input → `Option` of a tuple with the same length
2839
+ * - Struct input → `Option` of a struct with the same keys
2840
+ * - Iterable input → `Option` of an `Array`
2841
+ * - Any `None` in the input → entire result is `None`
2842
+ *
2843
+ * **Example** (Combining a tuple and a struct)
2844
+ *
2845
+ * ```ts import.meta.vitest
2846
+ * import { Option } from "effect"
2847
+ *
2848
+ * const maybeName: Option.Option<string> = Option.some("John")
2849
+ * const maybeAge: Option.Option<number> = Option.some(25)
2850
+ *
2851
+ * // ┌─── Option<[string, number]>
2852
+ * // ▼
2853
+ * const tuple = Option.all([maybeName, maybeAge]) // => Option.some(["John", 25])
2854
+ *
2855
+ * // ┌─── Option<{ name: string; age: number; }>
2856
+ * // ▼
2857
+ * const struct = Option.all({ name: maybeName, age: maybeAge }) // => Option.some({ name: "John", age: 25 })
2858
+ * ```
2859
+ *
2860
+ * @see {@link product} for combining exactly two
2861
+ * @see {@link productMany} for a homogeneous collection
2862
+ *
2863
+ * @category combining
2864
+ * @since 2.0.0
2865
+ */
2866
+ const all = (input) => {
2867
+ if (Symbol.iterator in input) {
2868
+ const out = [];
2869
+ for (const o of input) {
2870
+ if (isNone(o)) return none();
2871
+ out.push(o.value);
2872
+ }
2873
+ return some$1(out);
2874
+ }
2875
+ const out = {};
2876
+ for (const key of Object.keys(input)) {
2877
+ const o = input[key];
2878
+ if (isNone(o)) return none();
2879
+ assignProperty(out, key, o.value);
2880
+ }
2881
+ return some$1(out);
2882
+ };
2883
+ /**
2828
2884
  * Filters an `Option` using a predicate. Returns `None` if the predicate is
2829
2885
  * not satisfied or the input is `None`.
2830
2886
  *
@@ -10280,7 +10336,7 @@ function collectIssues(checks, value, issues, ast, options) {
10280
10336
  function getConstructorDescriptor(ast) {
10281
10337
  if (!isDeclaration(ast)) return void 0;
10282
10338
  const getDescriptor = ast.annotations?.[CONSTRUCTOR_ANNOTATION_KEY];
10283
- return isFunction$2(getDescriptor) ? getDescriptor(ast.typeParameters) : void 0;
10339
+ return isFunction$3(getDescriptor) ? getDescriptor(ast.typeParameters) : void 0;
10284
10340
  }
10285
10341
  /**
10286
10342
  * Creates an effectful decoder for `unknown` input.
@@ -11736,9 +11792,9 @@ const isCallExpression$1 = (node) => typeof node === "object" && node !== null &
11736
11792
  const isMemberExpression = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "MemberExpression";
11737
11793
  const isObjectExpression = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ObjectExpression";
11738
11794
  const isObjectProperty = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "Property";
11739
- const isArrayExpression = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ArrayExpression";
11795
+ const isArrayExpression$1 = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ArrayExpression";
11740
11796
  const isSpreadElement = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "SpreadElement";
11741
- const isArrowFunction = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ArrowFunctionExpression";
11797
+ const isArrowFunction$1 = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ArrowFunctionExpression";
11742
11798
  const helperCalleeName = (callee) => {
11743
11799
  if (isIdentifier(callee)) return some$1(callee.name);
11744
11800
  if (isMemberExpression(callee) && !callee.computed && isIdentifier(callee.property)) return some$1(callee.property.name);
@@ -11885,7 +11941,7 @@ const isEffectCall = (node, memberName, references) => {
11885
11941
  const isSyncReturningIdentifier = (node, references) => {
11886
11942
  if (!isEffectCall(node, "sync", references)) return false;
11887
11943
  const [thunk] = node.arguments;
11888
- return isArrowFunction(thunk) && isIdentifier(thunk.body);
11944
+ return isArrowFunction$1(thunk) && isIdentifier(thunk.body);
11889
11945
  };
11890
11946
  const isEagerSucceed = (node, references) => isEffectCall(node, "succeed", references);
11891
11947
  const ACQUIRE_CAPTURED_HANDLE_MESSAGE = "Effect.acquireRelease must construct its resource inside the acquire Effect, not return an eagerly created or captured handle. An interruption between construction and acquire leaks the handle. Build it in place with Effect.sync or another lazy Effect so acquire owns the whole lifetime.";
@@ -12216,7 +12272,7 @@ const singleValueMapNamespaces = [
12216
12272
  ];
12217
12273
  const isArrowFunctionExpression$3 = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ArrowFunctionExpression";
12218
12274
  const isBlockStatement$2 = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "BlockStatement";
12219
- const isReturnStatement$2 = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ReturnStatement";
12275
+ const isReturnStatement$4 = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ReturnStatement";
12220
12276
  const isObjectPattern = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "ObjectPattern";
12221
12277
  const isSingleValueMapCallee = (callee, references) => {
12222
12278
  if (references === void 0) return isMemberExpression(callee) && !callee.computed && isIdentifier(callee.property, "map") && isIdentifier(callee.object) && singleValueMapNamespaces.includes(callee.object.name);
@@ -12259,12 +12315,12 @@ const isIdentityBearing = (callback, references, declaredVariables) => {
12259
12315
  };
12260
12316
  const callbackReturnExpression = (callback) => {
12261
12317
  if (!isBlockStatement$2(callback.body)) return some$1(callback.body);
12262
- return pipe(callback.body.body, findLast(isReturnStatement$2), flatMap$3((returnStatement) => fromNullishOr(returnStatement.argument)));
12318
+ return pipe(callback.body.body, findLast(isReturnStatement$4), flatMap$3((returnStatement) => fromNullishOr(returnStatement.argument)));
12263
12319
  };
12264
12320
  const isKeyedWrapped = (node, references) => isCallExpression$1(node.callee) && isFoldkitHtmlBuilderMember(node.callee.callee, "keyed", references);
12265
12321
  const hasKeyAttribute = (rowElementCall, references) => {
12266
12322
  const [attributesArgument] = rowElementCall.arguments;
12267
- if (!isArrayExpression(attributesArgument)) return false;
12323
+ if (!isArrayExpression$1(attributesArgument)) return false;
12268
12324
  return attributesArgument.elements.some((element) => isCallExpression$1(element) && isFoldkitHtmlBuilderMember(element.callee, "Key", references));
12269
12325
  };
12270
12326
  const unkeyedRowElement = (returnExpression, references) => {
@@ -12696,7 +12752,7 @@ const messageModuleSegmentNames = /* @__PURE__ */ new Set([
12696
12752
  "schema"
12697
12753
  ]);
12698
12754
  const isMessageModuleSegment = (segment) => segment !== void 0 && messageModuleSegmentNames.has(moduleSegmentName(segment).toLowerCase());
12699
- const isFunction$1 = (node) => typeof node === "object" && node !== null && "type" in node && (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression");
12755
+ const isFunction$2 = (node) => typeof node === "object" && node !== null && "type" in node && (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression");
12700
12756
  const isTypeReference = (node) => typeof node === "object" && node !== null && "type" in node && node.type === "TSTypeReference";
12701
12757
  const isTypeAnnotatedIdentifier = (node) => isIdentifier(node);
12702
12758
  const importedMessagePath = (references, node) => isTypeReference(node) ? map$5(resolveImportedPath(references, node.typeName), (path) => ({
@@ -12751,7 +12807,7 @@ const defineViewMessagePath = (references, functionNode) => {
12751
12807
  const isOwnedByEnclosingView = (node, references, messagePath) => {
12752
12808
  const ownsMessage = (ancestor) => {
12753
12809
  if (ancestor === null || ancestor === void 0) return false;
12754
- if (isFunction$1(ancestor)) {
12810
+ if (isFunction$2(ancestor)) {
12755
12811
  if (ancestor.params.some((parameter) => exists(htmlBuilderMessagePath(references, parameter), (builderMessagePath) => sameImportedMessagePath(builderMessagePath, messagePath)))) return true;
12756
12812
  if (exists(defineViewMessagePath(references, ancestor), (viewMessagePath) => sameImportedMessagePath(viewMessagePath, messagePath))) return true;
12757
12813
  }
@@ -12784,13 +12840,31 @@ const childNamespace = (source, importedMembers, localMembers) => {
12784
12840
  const [localRoot] = localMembers;
12785
12841
  return localRoot ?? "Child";
12786
12842
  };
12843
+ const resolveImportedPathThroughConst = (references, node, visited = /* @__PURE__ */ new Set()) => {
12844
+ const importedPath = resolveImportedPath(references, node);
12845
+ if (isSome(importedPath)) return importedPath;
12846
+ return gen$2(function* () {
12847
+ const path = yield* staticMemberPath(node);
12848
+ const variable = yield* resolvedVariable(references, path.root);
12849
+ if (visited.has(variable) || length(variable.defs) !== 1) return yield* none();
12850
+ const [definition] = variable.defs;
12851
+ if (definition === void 0 || definition.type !== "Variable" || !isVariableDeclarator$1(definition.node) || !isVariableDeclaration$1(definition.node.parent) || definition.node.parent.kind !== "const" || !isIdentifier(definition.node.id)) return yield* none();
12852
+ const initializer = yield* fromNullishOr(definition.node.init);
12853
+ const resolved = yield* resolveImportedPathThroughConst(references, initializer, /* @__PURE__ */ new Set([...visited, variable]));
12854
+ return {
12855
+ source: resolved.source,
12856
+ members: [...resolved.members, ...path.members],
12857
+ localMembers: [path.root.name, ...path.members]
12858
+ };
12859
+ });
12860
+ };
12787
12861
  const childMessageConstruction = (callee, references) => {
12788
12862
  if (hasComputedMember(callee)) return none();
12789
12863
  if (references === void 0) return flatMap$3(staticMemberPath(callee), (path) => {
12790
12864
  const [namespace, middle, constructorName, extraSegment] = [path.root.name, ...path.members];
12791
12865
  return namespace !== void 0 && middle === "Message" && constructorName !== void 0 && constructorName !== "Message" && extraSegment === void 0 && pascalIdentifierPattern.test(namespace) && pascalIdentifierPattern.test(constructorName) ? some$1({ calleeLabel: `${namespace}.Message.${constructorName}` }) : none();
12792
12866
  });
12793
- return flatMap$3(resolveImportedPath(references, callee), (path) => {
12867
+ return flatMap$3(resolveImportedPathThroughConst(references, callee), (path) => {
12794
12868
  const [messageNamespace, constructorName] = path.members.slice(-2);
12795
12869
  if (messageNamespace !== "Message" || constructorName === void 0 || constructorName === "Message" || !pascalIdentifierPattern.test(constructorName) || isRootMessageModule(path.source)) return none();
12796
12870
  return some$1({
@@ -12803,8 +12877,8 @@ const childMessageConstruction = (callee, references) => {
12803
12877
  });
12804
12878
  };
12805
12879
  /**
12806
- * Flags direct construction of a child Submodel Message variant from outside
12807
- * the child, such as Chat.Message.ClickedOpen(...).
12880
+ * Flags construction of a child Submodel Message variant from outside the
12881
+ * child, such as Chat.Message.ClickedOpen(...) or an immutable local alias.
12808
12882
  */
12809
12883
  const noChildMessageConstructionInRoot = Rule_exports.define({
12810
12884
  name: "no-child-message-construction-in-root",
@@ -12832,6 +12906,329 @@ const noChildMessageConstructionInRoot = Rule_exports.define({
12832
12906
  }
12833
12907
  });
12834
12908
  //#endregion
12909
+ //#region src/rules/no-direct-submodel-state-update.ts
12910
+ const directSubmodelStateUpdateMessage = (fieldName) => `Do not update the Submodel field \`${fieldName}\` directly from its parent. Expose a child-owned update capability, then apply it with Update.foldChild or Update.foldChildStep.`;
12911
+ const isNode$2 = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
12912
+ const isArrowFunction = (value) => isNode$2(value) && value.type === "ArrowFunctionExpression";
12913
+ const isReturnStatement$3 = (value) => isNode$2(value) && value.type === "ReturnStatement";
12914
+ const walk$1 = (root, visit) => {
12915
+ const visited = /* @__PURE__ */ new WeakSet();
12916
+ const visitNode = (node, ancestors) => {
12917
+ if (visited.has(node)) return;
12918
+ visited.add(node);
12919
+ visit(node, ancestors);
12920
+ for (const [key, value] of Object.entries(node)) {
12921
+ if (key === "parent") continue;
12922
+ if (isNode$2(value)) visitNode(value, [...ancestors, node]);
12923
+ else if (globalThis.Array.isArray(value)) {
12924
+ for (const entry of value) if (isNode$2(entry)) visitNode(entry, [...ancestors, node]);
12925
+ }
12926
+ }
12927
+ };
12928
+ visitNode(root, []);
12929
+ };
12930
+ const expressionReturnedBy = (callback) => {
12931
+ if (callback.body.type !== "BlockStatement") return callback.body;
12932
+ for (const statement of callback.body.body) if (isReturnStatement$3(statement) && statement.argument !== null) return statement.argument;
12933
+ };
12934
+ const isFunction$1 = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration";
12935
+ const expressionsReturnedBy = (callback) => {
12936
+ if (callback.body.type !== "BlockStatement") return [callback.body];
12937
+ const returned = [];
12938
+ const visit = (node) => {
12939
+ if (isFunction$1(node)) return;
12940
+ if (isReturnStatement$3(node) && node.argument !== null) {
12941
+ returned.push(node.argument);
12942
+ return;
12943
+ }
12944
+ for (const [key, value] of Object.entries(node)) {
12945
+ if (key === "parent") continue;
12946
+ if (isNode$2(value)) visit(value);
12947
+ else if (globalThis.Array.isArray(value)) {
12948
+ for (const entry of value) if (isNode$2(entry)) visit(entry);
12949
+ }
12950
+ }
12951
+ };
12952
+ for (const statement of callback.body.body) visit(statement);
12953
+ return returned;
12954
+ };
12955
+ const directMemberField = (expression, parameterName) => {
12956
+ if (!isMemberExpression(expression) || expression.computed) return;
12957
+ if (!isIdentifier(expression.object, parameterName)) return;
12958
+ return isIdentifier(expression.property) ? expression.property.name : void 0;
12959
+ };
12960
+ const directReadField = (callback) => {
12961
+ const [modelParameter] = callback.params;
12962
+ if (!isIdentifier(modelParameter)) return;
12963
+ const returned = expressionReturnedBy(callback);
12964
+ if (!isCallExpression$1(returned)) return;
12965
+ const [readValue] = returned.arguments;
12966
+ return directMemberField(readValue, modelParameter.name);
12967
+ };
12968
+ const propertyValue = (object, name) => {
12969
+ for (const property of object.properties) if (property.type === "Property" && !property.computed && isIdentifier(property.key, name)) return property.value;
12970
+ };
12971
+ const directWriteField = (callback, references) => {
12972
+ const [modelParameter, nextChildParameter] = callback.params;
12973
+ if (!isIdentifier(modelParameter) || !isIdentifier(nextChildParameter)) return;
12974
+ const returned = expressionReturnedBy(callback);
12975
+ if (!isCallExpression$1(returned) || !isEvoCall$1(returned, references)) return;
12976
+ const [evolvedModel, updates] = returned.arguments;
12977
+ if (!isIdentifier(evolvedModel, modelParameter.name) || !isObjectExpression(updates)) return;
12978
+ for (const property of updates.properties) {
12979
+ if (property.type !== "Property" || property.computed || !isIdentifier(property.key) || !isArrowFunction(property.value)) continue;
12980
+ const replacement = expressionReturnedBy(property.value);
12981
+ if (isIdentifier(replacement, nextChildParameter.name)) return property.key.name;
12982
+ }
12983
+ };
12984
+ const isEvoCall$1 = (node, references) => {
12985
+ if (references === void 0) return exists(staticMemberPath(node.callee), (path) => {
12986
+ const [namespace, helperName, extraMember] = [path.root.name, ...path.members];
12987
+ return namespace === "Struct" && helperName === "evo" && extraMember === void 0;
12988
+ }) || isIdentifier(node.callee, "evo");
12989
+ return exists(resolveFoldkitApiPath(references, node.callee), (path) => {
12990
+ const [namespace, helperName, extraMember] = path;
12991
+ return namespace === "Struct" && helperName === "evo" && extraMember === void 0;
12992
+ });
12993
+ };
12994
+ const foldKind = (node, references) => {
12995
+ const foldKindForName = (helperName) => {
12996
+ if (helperName === "foldChild") return "Fold";
12997
+ if (helperName === "foldChildStep") return "Step";
12998
+ };
12999
+ if (references === void 0) return getOrUndefined(flatMap$3(staticMemberPath(node.callee), (path) => {
13000
+ const [namespace, helperName, extraMember] = [path.root.name, ...path.members];
13001
+ if (namespace !== "Update" || extraMember !== void 0) return none();
13002
+ if (helperName === void 0) return none();
13003
+ return fromNullishOr(foldKindForName(helperName));
13004
+ }));
13005
+ return getOrUndefined(flatMap$3(resolveFoldkitApiPath(references, node.callee), (path) => {
13006
+ const [namespace, helperName, extraMember] = path;
13007
+ if (namespace !== "Update" || extraMember !== void 0) return none();
13008
+ if (helperName === void 0) return none();
13009
+ return fromNullishOr(foldKindForName(helperName));
13010
+ }));
13011
+ };
13012
+ const isCombineCall = (node, references) => {
13013
+ if (references === void 0) return exists(staticMemberPath(node.callee), (path) => {
13014
+ const [namespace, helperName, extraMember] = [path.root.name, ...path.members];
13015
+ return namespace === "Update" && helperName === "combine" && extraMember === void 0;
13016
+ });
13017
+ return exists(resolveFoldkitApiPath(references, node.callee), (path) => {
13018
+ const [namespace, helperName, extraMember] = path;
13019
+ return namespace === "Update" && helperName === "combine" && extraMember === void 0;
13020
+ });
13021
+ };
13022
+ const moduleCallbacks$1 = (program) => {
13023
+ const callbacks = /* @__PURE__ */ new Map();
13024
+ for (const statement of program.body) {
13025
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
13026
+ if (declaration?.type !== "VariableDeclaration") continue;
13027
+ for (const declarator of declaration.declarations) if (isIdentifier(declarator.id) && isArrowFunction(declarator.init)) callbacks.set(declarator.id.name, declarator.init);
13028
+ }
13029
+ return callbacks;
13030
+ };
13031
+ const callbackValue$1 = (value, callbacks) => isArrowFunction(value) ? value : isIdentifier(value) ? callbacks.get(value.name) : void 0;
13032
+ const addChildField = (fieldsByParentModel, parentModel, childField) => {
13033
+ const fields = fieldsByParentModel.get(parentModel);
13034
+ if (fields === void 0) {
13035
+ fieldsByParentModel.set(parentModel, /* @__PURE__ */ new Set([childField]));
13036
+ return;
13037
+ }
13038
+ fields.add(childField);
13039
+ };
13040
+ const foldDefinitions = (program, scopes, references) => {
13041
+ const callbacks = moduleCallbacks$1(program);
13042
+ const byVariable = /* @__PURE__ */ new WeakMap();
13043
+ const fallbackByName = /* @__PURE__ */ new Map();
13044
+ const writeCallbacks = /* @__PURE__ */ new Set();
13045
+ for (const statement of program.body) {
13046
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
13047
+ if (declaration?.type !== "VariableDeclaration") continue;
13048
+ for (const declarator of declaration.declarations) {
13049
+ if (!isIdentifier(declarator.id) || !isCallExpression$1(declarator.init)) continue;
13050
+ const kind = foldKind(declarator.init, references);
13051
+ if (kind === void 0) continue;
13052
+ const [configuration] = declarator.init.arguments;
13053
+ if (!isObjectExpression(configuration)) continue;
13054
+ const read = callbackValue$1(propertyValue(configuration, "read"), callbacks);
13055
+ const write = callbackValue$1(propertyValue(configuration, "write"), callbacks);
13056
+ if (read === void 0 || write === void 0) continue;
13057
+ writeCallbacks.add(write);
13058
+ const readField = directReadField(read);
13059
+ const writeField = directWriteField(write, references);
13060
+ if (readField === void 0 || readField !== writeField) continue;
13061
+ const definition = {
13062
+ childField: readField,
13063
+ kind
13064
+ };
13065
+ fallbackByName.set(declarator.id.name, definition);
13066
+ for (const scope of scopes ?? []) for (const variable of scope.variables) if (variable.defs.some((candidate) => candidate.node === declarator)) byVariable.set(variable, definition);
13067
+ }
13068
+ }
13069
+ return {
13070
+ byVariable,
13071
+ fallbackByName,
13072
+ writeCallbacks
13073
+ };
13074
+ };
13075
+ const foldDefinitionFor = (callee, definitions, references) => {
13076
+ if (!isIdentifierReference(callee)) return;
13077
+ if (references === void 0) return definitions.fallbackByName.get(callee.name);
13078
+ return getOrUndefined(flatMap$3(resolvedVariable(references, callee), (variable) => fromNullishOr(definitions.byVariable.get(variable))));
13079
+ };
13080
+ const isArrayExpression = (value) => isNode$2(value) && value.type === "ArrayExpression";
13081
+ const childFieldsForCombine = (node, definitions, references) => {
13082
+ if (!isCombineCall(node, references)) return /* @__PURE__ */ new Set();
13083
+ const [firstArgument, secondArgument] = node.arguments;
13084
+ const steps = isArrayExpression(firstArgument) ? firstArgument : isArrayExpression(secondArgument) ? secondArgument : void 0;
13085
+ if (steps === void 0) return /* @__PURE__ */ new Set();
13086
+ const childFields = /* @__PURE__ */ new Set();
13087
+ for (const step of steps.elements) {
13088
+ if (isIdentifierReference(step)) {
13089
+ const definition = foldDefinitionFor(step, definitions, references);
13090
+ if (definition?.kind === "Step") childFields.add(definition.childField);
13091
+ continue;
13092
+ }
13093
+ if (!isCallExpression$1(step)) continue;
13094
+ const definition = foldDefinitionFor(step.callee, definitions, references);
13095
+ if (definition?.kind === "Fold") childFields.add(definition.childField);
13096
+ }
13097
+ return childFields;
13098
+ };
13099
+ const combineStepDefinitions = (program, scopes, definitions, references) => {
13100
+ const byVariable = /* @__PURE__ */ new WeakMap();
13101
+ const fallbackByName = /* @__PURE__ */ new Map();
13102
+ walk$1(program, (node) => {
13103
+ if (node.type !== "VariableDeclarator" || !isIdentifier(node.id) || !isCallExpression$1(node.init) || !isCombineCall(node.init, references)) return;
13104
+ const [firstArgument, secondArgument] = node.init.arguments;
13105
+ if (!isArrayExpression(firstArgument) || secondArgument !== void 0) return;
13106
+ const childFields = childFieldsForCombine(node.init, definitions, references);
13107
+ if (childFields.size === 0) return;
13108
+ const definition = { childFields };
13109
+ fallbackByName.set(node.id.name, definition);
13110
+ for (const scope of scopes ?? []) for (const variable of scope.variables) if (variable.defs.some((candidate) => candidate.node === node)) byVariable.set(variable, definition);
13111
+ });
13112
+ return {
13113
+ byVariable,
13114
+ fallbackByName
13115
+ };
13116
+ };
13117
+ const combineStepDefinitionFor = (callee, definitions, references) => {
13118
+ if (!isIdentifierReference(callee)) return;
13119
+ if (references === void 0) return definitions.fallbackByName.get(callee.name);
13120
+ return getOrUndefined(flatMap$3(resolvedVariable(references, callee), (variable) => fromNullishOr(definitions.byVariable.get(variable))));
13121
+ };
13122
+ const isDataLastFoldCall = (node, ancestors) => ancestors.some((ancestor) => isCallExpression$1(ancestor) && ancestor.callee === node);
13123
+ const invokedModel = (node, ancestors) => {
13124
+ const invocation = ancestors.find((candidate) => isCallExpression$1(candidate) && candidate.callee === node);
13125
+ if (invocation === void 0 || !isCallExpression$1(invocation)) return;
13126
+ const [parentModel] = invocation.arguments;
13127
+ return isIdentifierReference(parentModel) ? parentModel : void 0;
13128
+ };
13129
+ const combineModel = (node, ancestors) => {
13130
+ const [firstArgument, secondArgument] = node.arguments;
13131
+ if (isIdentifierReference(firstArgument) && isArrayExpression(secondArgument)) return firstArgument;
13132
+ return isArrayExpression(firstArgument) ? invokedModel(node, ancestors) : void 0;
13133
+ };
13134
+ const addDefinitionEvidence = (definition, parentModel, childFieldsByParentModel, fallbackChildFieldsByParentModel, references) => {
13135
+ if (references === void 0) {
13136
+ addChildField(fallbackChildFieldsByParentModel, parentModel.name, definition.childField);
13137
+ return;
13138
+ }
13139
+ map$5(resolvedVariable(references, parentModel), (variable) => addChildField(childFieldsByParentModel, variable, definition.childField));
13140
+ };
13141
+ const foldEvidence = (program, scopes, references) => {
13142
+ const definitions = foldDefinitions(program, scopes, references);
13143
+ const stepDefinitions = combineStepDefinitions(program, scopes, definitions, references);
13144
+ const childFieldsByParentModel = /* @__PURE__ */ new Map();
13145
+ const fallbackChildFieldsByParentModel = /* @__PURE__ */ new Map();
13146
+ walk$1(program, (node, ancestors) => {
13147
+ if (!isCallExpression$1(node)) return;
13148
+ const parentModelForCombine = isCombineCall(node, references) ? combineModel(node, ancestors) : void 0;
13149
+ if (parentModelForCombine !== void 0) for (const childField of childFieldsForCombine(node, definitions, references)) addDefinitionEvidence({
13150
+ childField,
13151
+ kind: "Step"
13152
+ }, parentModelForCombine, childFieldsByParentModel, fallbackChildFieldsByParentModel, references);
13153
+ if (isDataLastFoldCall(node, ancestors)) return;
13154
+ const stepDefinition = combineStepDefinitionFor(node.callee, stepDefinitions, references);
13155
+ if (stepDefinition !== void 0) {
13156
+ const [parentModel] = node.arguments;
13157
+ if (!isIdentifierReference(parentModel)) return;
13158
+ for (const childField of stepDefinition.childFields) addDefinitionEvidence({
13159
+ childField,
13160
+ kind: "Step"
13161
+ }, parentModel, childFieldsByParentModel, fallbackChildFieldsByParentModel, references);
13162
+ return;
13163
+ }
13164
+ const directDefinition = foldDefinitionFor(node.callee, definitions, references);
13165
+ const dataLastDefinition = isCallExpression$1(node.callee) ? foldDefinitionFor(node.callee.callee, definitions, references) : void 0;
13166
+ if (directDefinition === void 0 && (dataLastDefinition === void 0 || dataLastDefinition.kind !== "Fold")) return;
13167
+ const [firstArgument, secondArgument] = node.arguments;
13168
+ if (directDefinition?.kind === "Fold" && secondArgument === void 0) return;
13169
+ const definition = directDefinition ?? dataLastDefinition;
13170
+ const parentModel = firstArgument;
13171
+ if (!isIdentifierReference(parentModel) || definition === void 0) return;
13172
+ addDefinitionEvidence(definition, parentModel, childFieldsByParentModel, fallbackChildFieldsByParentModel, references);
13173
+ });
13174
+ return {
13175
+ childFieldsByParentModel,
13176
+ fallbackChildFieldsByParentModel,
13177
+ writeCallbacks: definitions.writeCallbacks
13178
+ };
13179
+ };
13180
+ const directChildEvoField = (node, evidence, references) => {
13181
+ if (!isEvoCall$1(node, references)) return;
13182
+ const [model, updates] = node.arguments;
13183
+ if (!isIdentifierReference(model) || !isObjectExpression(updates)) return;
13184
+ const childFields = references === void 0 ? evidence.fallbackChildFieldsByParentModel.get(model.name) : getOrUndefined(flatMap$3(resolvedVariable(references, model), (variable) => fromNullishOr(evidence.childFieldsByParentModel.get(variable))));
13185
+ if (childFields === void 0) return;
13186
+ for (const property of updates.properties) {
13187
+ if (property.type !== "Property" || property.computed || !isIdentifier(property.key) || !childFields.has(property.key.name) || !isArrowFunction(property.value)) continue;
13188
+ const [childModel] = property.value.params;
13189
+ if (!isIdentifier(childModel)) continue;
13190
+ for (const nestedUpdate of expressionsReturnedBy(property.value)) {
13191
+ if (!isCallExpression$1(nestedUpdate) || !isEvoCall$1(nestedUpdate, references)) continue;
13192
+ const [nestedModel] = nestedUpdate.arguments;
13193
+ if (isIdentifier(nestedModel, childModel.name)) return property.key.name;
13194
+ }
13195
+ }
13196
+ };
13197
+ /** Flags parent-owned evo calls that directly update a child Submodel Model. */
13198
+ const noDirectSubmodelStateUpdate = Rule_exports.define({
13199
+ name: "no-direct-submodel-state-update",
13200
+ meta: Rule_exports.meta({
13201
+ type: "suggestion",
13202
+ description: "Keep Submodel state changes inside child-owned update capabilities."
13203
+ }),
13204
+ create: function* () {
13205
+ const ctx = yield* RuleContext;
13206
+ const scopes = ctx.sourceCode.scopeManager?.scopes;
13207
+ const references = scopes === void 0 ? void 0 : indexReferences(scopes);
13208
+ return { "Program:exit": (node) => {
13209
+ if (node.type !== "Program") return void_;
13210
+ const evidence = foldEvidence(node, scopes, references);
13211
+ if (evidence.childFieldsByParentModel.size === 0 && evidence.fallbackChildFieldsByParentModel.size === 0) return void_;
13212
+ return forEach((() => {
13213
+ const findings = [];
13214
+ walk$1(node, (candidate, ancestors) => {
13215
+ if (!isCallExpression$1(candidate)) return;
13216
+ if (ancestors.some((ancestor) => evidence.writeCallbacks.has(ancestor))) return;
13217
+ const fieldName = directChildEvoField(candidate, evidence, references);
13218
+ if (fieldName !== void 0) findings.push({
13219
+ node: candidate,
13220
+ fieldName
13221
+ });
13222
+ });
13223
+ return findings;
13224
+ })(), ({ node: findingNode, fieldName }) => ctx.report(Diagnostic_exports.make({
13225
+ node: findingNode,
13226
+ message: directSubmodelStateUpdateMessage(fieldName)
13227
+ })), { discard: true });
13228
+ } };
13229
+ }
13230
+ });
13231
+ //#endregion
12835
13232
  //#region src/rules/no-disabling-dev-guardrails.ts
12836
13233
  const FACTORY_NAMES = ["makeApplication", "makeElement"];
12837
13234
  const FREEZE_MODEL_MESSAGE = "`freezeModel: false` turns off Foldkit's dev mutation guard. When on, the runtime deep-freezes the Model after init and after every update, so an accidental mutation throws at the exact write site during development and costs nothing in production. Fix the mutation instead of disabling the guard. A deliberate disable belongs behind an oxlint-disable comment at this line.";
@@ -12913,7 +13310,7 @@ const noDuplicateOnmountPerElement = Rule_exports.define({
12913
13310
  const scopes = ctx.sourceCode.scopeManager?.scopes;
12914
13311
  const references = scopes === void 0 ? void 0 : indexReferences(scopes);
12915
13312
  return { ArrayExpression: (node) => {
12916
- if (!isArrayExpression(node)) return void_;
13313
+ if (!isArrayExpression$1(node)) return void_;
12917
13314
  if (node.elements.filter((element) => isOnMountCall(element, references)).length <= 1) return void_;
12918
13315
  return ctx.report(Diagnostic_exports.make({
12919
13316
  node,
@@ -13191,7 +13588,7 @@ const noEmptyChildrenArray = Rule_exports.define({
13191
13588
  const maybeTarget = orElse(elementCallTarget(node, references), () => keyedCallTarget(node, references));
13192
13589
  if (isNone(maybeTarget)) return void_;
13193
13590
  const maybeChildren = childrenArgument(node);
13194
- if (isNone(maybeChildren) || !isArrayExpression(maybeChildren.value) || isReadonlyArrayNonEmpty(maybeChildren.value.elements)) return void_;
13591
+ if (isNone(maybeChildren) || !isArrayExpression$1(maybeChildren.value) || isReadonlyArrayNonEmpty(maybeChildren.value.elements)) return void_;
13195
13592
  if (isReadonlyArrayNonEmpty(ctx.sourceCode.getCommentsInside(maybeChildren.value))) return void_;
13196
13593
  return ctx.report(Diagnostic_exports.make({
13197
13594
  node: maybeChildren.value,
@@ -13203,7 +13600,7 @@ const noEmptyChildrenArray = Rule_exports.define({
13203
13600
  //#endregion
13204
13601
  //#region src/rules/no-empty-commands-array.ts
13205
13602
  const COMMANDS_PROPERTY = "commands";
13206
- const isEmptyCommandsProperty = (property) => contains$1(staticPropertyName(property), COMMANDS_PROPERTY) && isArrayExpression(property.value) && isReadonlyArrayEmpty(property.value.elements);
13603
+ const isEmptyCommandsProperty = (property) => contains$1(staticPropertyName(property), COMMANDS_PROPERTY) && isArrayExpression$1(property.value) && isReadonlyArrayEmpty(property.value.elements);
13207
13604
  const removalRange$1 = (sourceText, object, property, index) => {
13208
13605
  const maybePrevious = get$1(object.properties, index - 1);
13209
13606
  if (isSome(maybePrevious)) return [maybePrevious.value.end, property.end];
@@ -14712,11 +15109,11 @@ const isEvoCall = (node, references) => {
14712
15109
  if (callee.type === "Identifier") return callee.name === "evo";
14713
15110
  return callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "evo";
14714
15111
  };
14715
- const isReturnStatement$1 = (statement) => statement.type === "ReturnStatement";
15112
+ const isReturnStatement$2 = (statement) => statement.type === "ReturnStatement";
14716
15113
  const updaterBodyObject = (updater) => {
14717
15114
  const body = updater.body;
14718
15115
  if (body.type === "ObjectExpression") return some$1(body);
14719
- if (body.type === "BlockStatement") return pipe(body.body, findFirst(isReturnStatement$1), flatMap$3((returnStatement) => fromNullishOr(returnStatement.argument)), filter$2(isObjectExpression));
15116
+ if (body.type === "BlockStatement") return pipe(body.body, findFirst(isReturnStatement$2), flatMap$3((returnStatement) => fromNullishOr(returnStatement.argument)), filter$2(isObjectExpression));
14720
15117
  return none();
14721
15118
  };
14722
15119
  const hasSpreadProperty = (objectExpression) => objectExpression.properties.some((property) => property.type === "SpreadElement");
@@ -14879,7 +15276,7 @@ const isMessageConstructorReference = (node, references) => {
14879
15276
  };
14880
15277
  const isMessageConstructorCall = (node, references) => isCallExpression$1(node) && isMessageConstructorReference(node.callee, references);
14881
15278
  const isMessageWrappingArrow = (node, references) => {
14882
- if (!isArrowFunction(node)) return false;
15279
+ if (!isArrowFunction$1(node)) return false;
14883
15280
  if (!isBlockStatement$1(node.body)) return isMessageConstructorCall(node.body, references);
14884
15281
  return node.body.body.some((statement) => statement.type === "ReturnStatement" && isMessageConstructorCall(statement.argument, references));
14885
15282
  };
@@ -14897,8 +15294,8 @@ const isPipeMethodCall = (node) => exists(staticMemberPath(node.callee), (path)
14897
15294
  const [member, extraMember] = path.members;
14898
15295
  return member === "pipe" && extraMember === void 0;
14899
15296
  });
14900
- const returnedExpression = (node) => {
14901
- if (!isArrowFunction(node)) return none();
15297
+ const returnedExpression$1 = (node) => {
15298
+ if (!isArrowFunction$1(node)) return none();
14902
15299
  if (!isBlockStatement$1(node.body)) return some$1(node.body);
14903
15300
  const returnStatements = node.body.body.filter((statement) => statement.type === "ReturnStatement");
14904
15301
  return match(returnStatements, {
@@ -14908,7 +15305,7 @@ const returnedExpression = (node) => {
14908
15305
  };
14909
15306
  const resultPathMapsMessage = (node, references) => {
14910
15307
  if (isEffectMapWrappingMessage(node, references)) return true;
14911
- const maybeReturnedExpression = returnedExpression(node);
15308
+ const maybeReturnedExpression = returnedExpression$1(node);
14912
15309
  if (isSome(maybeReturnedExpression)) return resultPathMapsMessage(maybeReturnedExpression.value, references);
14913
15310
  if (!isCallExpression$1(node)) return false;
14914
15311
  if (isPipeCall(node, references)) return node.arguments.some((argument) => resultPathMapsMessage(argument, references));
@@ -15034,7 +15431,7 @@ const NULLABLE_MEMBERS = /* @__PURE__ */ new Set([
15034
15431
  "optional",
15035
15432
  "optionalKey"
15036
15433
  ]);
15037
- const isNode = (node) => typeof node === "object" && node !== null && "type" in node && typeof node.type === "string";
15434
+ const isNode$1 = (node) => typeof node === "object" && node !== null && "type" in node && typeof node.type === "string";
15038
15435
  const schemaMemberName = (node, references) => {
15039
15436
  if (references === void 0) return flatMap$3(staticMemberPath(node), (path) => {
15040
15437
  const [member, extraMember] = path.members;
@@ -15049,7 +15446,7 @@ const schemaMemberName = (node, references) => {
15049
15446
  };
15050
15447
  const isSchemaStructCall = (node, references) => isCallExpression$1(node) && contains$1(schemaMemberName(node.callee, references), "Struct");
15051
15448
  const nullableMember = (node, references) => {
15052
- if (!isNode(node) || !exists(schemaMemberName(node, references), (name) => NULLABLE_MEMBERS.has(name))) return none();
15449
+ if (!isNode$1(node) || !exists(schemaMemberName(node, references), (name) => NULLABLE_MEMBERS.has(name))) return none();
15053
15450
  return some$1(node);
15054
15451
  };
15055
15452
  const nullableFieldMember = (value, references) => {
@@ -15099,6 +15496,225 @@ const preferOptionOverNullableInModel = Rule_exports.define({
15099
15496
  }
15100
15497
  });
15101
15498
  //#endregion
15499
+ //#region src/rules/require-fold-for-child-update-result.ts
15500
+ const sameNames = (actual, expected) => length(actual) === length(expected) && every(zip(actual, expected), ([actualName, expectedName]) => actualName === expectedName);
15501
+ const sameIdentifierName = (reference, binding) => reference.name === binding.name;
15502
+ const isFoldkitEvoCall = (node, references) => {
15503
+ if (references !== void 0) return exists(resolveFoldkitApiPath(references, node.callee), (path) => sameNames(path, ["Struct", "evo"]));
15504
+ return exists(staticMemberPath(node.callee), (path) => path.root.name === "evo" && isReadonlyArrayEmpty(path.members) || path.root.name === "Struct" && sameNames(path.members, ["evo"]));
15505
+ };
15506
+ const isFoldCall = (node, references) => {
15507
+ const isFoldPath = (path) => sameNames(path, ["Update", "foldChild"]) || sameNames(path, ["Update", "foldChildStep"]);
15508
+ if (references !== void 0) return exists(resolveFoldkitApiPath(references, node.callee), isFoldPath);
15509
+ return exists(staticMemberPath(node.callee), (path) => isFoldPath([path.root.name, ...path.members]));
15510
+ };
15511
+ const isInsideFoldWrite = (node, references) => {
15512
+ let ancestor = node.parent;
15513
+ while (ancestor !== null && ancestor !== void 0) {
15514
+ if (isObjectProperty(ancestor) && contains$1(staticPropertyName(ancestor), "write") && isObjectExpression(ancestor.parent) && isCallExpression$1(ancestor.parent.parent) && isFoldCall(ancestor.parent.parent, references)) return true;
15515
+ ancestor = ancestor.parent;
15516
+ }
15517
+ return false;
15518
+ };
15519
+ const isNode = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
15520
+ const walk = (root, visit) => {
15521
+ const visited = /* @__PURE__ */ new WeakSet();
15522
+ const visitNode = (node) => {
15523
+ if (visited.has(node)) return;
15524
+ visited.add(node);
15525
+ visit(node);
15526
+ for (const [key, value] of Object.entries(node)) {
15527
+ if (key === "parent") continue;
15528
+ if (isNode(value)) visitNode(value);
15529
+ else if (globalThis.Array.isArray(value)) {
15530
+ for (const item of value) if (isNode(item)) visitNode(item);
15531
+ }
15532
+ }
15533
+ };
15534
+ visitNode(root);
15535
+ };
15536
+ const isReturnStatement$1 = (node) => isNode(node) && node.type === "ReturnStatement";
15537
+ const returnedExpression = (callback) => {
15538
+ if (callback.body.type !== "BlockStatement") return callback.body;
15539
+ const returnStatements = filter$1(callback.body.body, isReturnStatement$1);
15540
+ if (length(returnStatements) !== 1) return;
15541
+ return pipe(head(returnStatements), flatMap$3((statement) => fromNullishOr(statement.argument)), getOrUndefined);
15542
+ };
15543
+ const modelFieldNames = (functionNode) => {
15544
+ const [parentModel] = functionNode.params;
15545
+ const returned = returnedExpression(functionNode);
15546
+ if (!isIdentifier(parentModel) || returned === void 0) return /* @__PURE__ */ new Set();
15547
+ const fieldNames = /* @__PURE__ */ new Set();
15548
+ walk(returned, (node) => {
15549
+ pipe(staticMemberPath(node), filter$2((path) => length(path.members) === 1 && sameIdentifierName(path.root, parentModel)), flatMap$3((path) => head(path.members)), map$5((fieldName) => fieldNames.add(fieldName)));
15550
+ });
15551
+ return fieldNames;
15552
+ };
15553
+ const evoFieldNames = (functionNode, references) => {
15554
+ const [parentModel, nextChild] = functionNode.params;
15555
+ if (!isIdentifier(parentModel) || !isIdentifier(nextChild)) return /* @__PURE__ */ new Set();
15556
+ const fieldNames = /* @__PURE__ */ new Set();
15557
+ walk(functionNode.body, (node) => {
15558
+ if (!isCallExpression$1(node) || !isFoldkitEvoCall(node, references)) return;
15559
+ const [evoModel, updates] = node.arguments;
15560
+ if (!isIdentifier(evoModel) || !sameIdentifierName(evoModel, parentModel) || !isObjectExpression(updates)) return;
15561
+ for (const property of updates.properties) {
15562
+ if (!isObjectProperty(property) || property.computed || !isArrowFunction$1(property.value)) continue;
15563
+ const replacement = returnedExpression(property.value);
15564
+ if (!isIdentifier(replacement) || !sameIdentifierName(replacement, nextChild)) continue;
15565
+ pipe(staticPropertyName(property), map$5((fieldName) => fieldNames.add(fieldName)));
15566
+ }
15567
+ });
15568
+ return fieldNames;
15569
+ };
15570
+ const moduleCallbacks = (program) => {
15571
+ const callbacks = /* @__PURE__ */ new Map();
15572
+ for (const statement of program.body) {
15573
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
15574
+ if (declaration?.type !== "VariableDeclaration") continue;
15575
+ for (const declarator of declaration.declarations) if (isIdentifier(declarator.id) && isArrowFunction$1(declarator.init)) callbacks.set(declarator.id.name, declarator.init);
15576
+ }
15577
+ return callbacks;
15578
+ };
15579
+ const callbackValue = (value, callbacks) => {
15580
+ if (isArrowFunction$1(value)) return value;
15581
+ if (isIdentifier(value)) return callbacks.get(value.name);
15582
+ };
15583
+ const foldChildFieldsByNamespace = (program, references) => {
15584
+ const fieldsByNamespace = /* @__PURE__ */ new Map();
15585
+ const callbacks = moduleCallbacks(program);
15586
+ walk(program, (node) => {
15587
+ if (!isCallExpression$1(node) || !isFoldCall(node, references)) return;
15588
+ const [configuration] = node.arguments;
15589
+ if (!isObjectExpression(configuration)) return;
15590
+ let namespace;
15591
+ let readFields;
15592
+ let writeFields;
15593
+ for (const property of configuration.properties) {
15594
+ if (!isObjectProperty(property)) continue;
15595
+ if (contains$1(staticPropertyName(property), "update")) pipe(staticMemberPath(property.value), filter$2((path) => length(path.members) === 1), map$5((path) => {
15596
+ namespace = path.root.name;
15597
+ }));
15598
+ else if (contains$1(staticPropertyName(property), "read")) {
15599
+ const callback = callbackValue(property.value, callbacks);
15600
+ if (callback !== void 0) readFields = modelFieldNames(callback);
15601
+ } else if (contains$1(staticPropertyName(property), "write")) {
15602
+ const callback = callbackValue(property.value, callbacks);
15603
+ if (callback !== void 0) writeFields = evoFieldNames(callback, references);
15604
+ }
15605
+ }
15606
+ if (namespace === void 0 || readFields === void 0 || writeFields === void 0) return;
15607
+ for (const fieldName of readFields) {
15608
+ if (!writeFields.has(fieldName)) continue;
15609
+ const fieldNames = fieldsByNamespace.get(namespace) ?? /* @__PURE__ */ new Set();
15610
+ fieldNames.add(fieldName);
15611
+ fieldsByNamespace.set(namespace, fieldNames);
15612
+ }
15613
+ });
15614
+ return fieldsByNamespace;
15615
+ };
15616
+ const childModelField = (node) => flatMap$3(staticMemberPath(node), (path) => {
15617
+ const [fieldName, extraFieldName] = path.members;
15618
+ return fieldName !== void 0 && extraFieldName === void 0 ? some$1({
15619
+ fieldName,
15620
+ parentModel: path.root
15621
+ }) : none();
15622
+ });
15623
+ const childReturnOrigin = (node, references, foldFieldsByNamespace) => gen$2(function* () {
15624
+ if (!isIdentifier(node.id) || !isCallExpression$1(node.init)) return yield* none();
15625
+ const childModel = yield* flatMap$3(head(node.init.arguments), childModelField);
15626
+ const callee = yield* staticMemberPath(node.init.callee);
15627
+ const [helperName, extraHelperName] = callee.members;
15628
+ if (helperName === void 0 || extraHelperName !== void 0) return yield* none();
15629
+ if (references !== void 0) {
15630
+ const importedPath = yield* resolveImportedPath(references, node.init.callee);
15631
+ const [namespace, importedHelperName, extraImportedHelperName] = importedPath.localMembers;
15632
+ if (!importedPath.source.startsWith(".") || namespace === void 0 || importedHelperName === void 0 || extraImportedHelperName !== void 0 || namespace !== callee.root.name || importedHelperName !== helperName) return yield* none();
15633
+ } else if (callee.root.name.charAt(0) !== callee.root.name.charAt(0).toUpperCase()) return yield* none();
15634
+ const foldFields = foldFieldsByNamespace.get(callee.root.name);
15635
+ if (foldFields === void 0 || !foldFields.has(childModel.fieldName)) return yield* none();
15636
+ return {
15637
+ childFieldName: childModel.fieldName,
15638
+ helperLabel: `${callee.root.name}.${helperName}`,
15639
+ parentModel: childModel.parentModel
15640
+ };
15641
+ });
15642
+ const resultBinding = (updater) => pipe(returnedExpression(updater), fromNullishOr, flatMap$3(staticMemberPath), flatMap$3((path) => sameNames(path.members, ["model"]) ? some$1(path.root) : none()));
15643
+ const sameBinding = (a, b, references) => {
15644
+ if (references === void 0) return a.name === b.name;
15645
+ const maybeA = resolvedVariable(references, a);
15646
+ const maybeB = resolvedVariable(references, b);
15647
+ return isSome(maybeA) && isSome(maybeB) ? maybeA.value === maybeB.value : a.name === b.name;
15648
+ };
15649
+ const manualChildResultMessage = (childFieldName, helperLabel, resultName) => `The parent writes \`${resultName}.model\` into \`${childFieldName}\` after calling \`${helperLabel}\`. A child Return can include Commands or an OutMessage that this copy ignores. Use Update.foldChild or Update.foldChildStep.`;
15650
+ /** Flags a parent that copies a namespaced child helper or update Return's Model
15651
+ * into the matching evo field. It requires a matching Update.foldChild or
15652
+ * Update.foldChildStep reference in the file, and leaves ordinary helpers,
15653
+ * direct imports, aliases, init assembly, and Model-only reflect helpers alone. */
15654
+ const requireFoldForChildUpdateResult = Rule_exports.define({
15655
+ name: "require-fold-for-child-update-result",
15656
+ meta: Rule_exports.meta({
15657
+ type: "suggestion",
15658
+ description: "Fold child update results instead of manually copying their Models into a parent."
15659
+ }),
15660
+ create: function* () {
15661
+ const ctx = yield* RuleContext;
15662
+ const scopes = ctx.sourceCode.scopeManager?.scopes;
15663
+ const references = scopes === void 0 ? void 0 : indexReferences(scopes);
15664
+ const originsByVariable = /* @__PURE__ */ new WeakMap();
15665
+ const fallbackOriginsByName = /* @__PURE__ */ new Map();
15666
+ let foldFieldsByNamespace = /* @__PURE__ */ new Map();
15667
+ return {
15668
+ Program: (node) => {
15669
+ if (!isProgram$1(node)) return void_;
15670
+ foldFieldsByNamespace = foldChildFieldsByNamespace(node, references);
15671
+ if (references === void 0) return void_;
15672
+ for (const scope of scopes ?? []) for (const variable of scope.variables) {
15673
+ const definition = variable.defs.find((candidate) => candidate.type === "Variable" && isVariableDeclarator$1(candidate.node));
15674
+ if (definition === void 0 || !isVariableDeclarator$1(definition.node)) continue;
15675
+ pipe(childReturnOrigin(definition.node, references, foldFieldsByNamespace), map$5((origin) => originsByVariable.set(variable, origin)));
15676
+ }
15677
+ return void_;
15678
+ },
15679
+ VariableDeclarator: (node) => {
15680
+ if (references !== void 0 || !isVariableDeclarator$1(node)) return void_;
15681
+ return pipe(childReturnOrigin(node, references, foldFieldsByNamespace), match$3({
15682
+ onNone: () => void_,
15683
+ onSome: (origin) => {
15684
+ if (isIdentifier(node.id)) fallbackOriginsByName.set(node.id.name, origin);
15685
+ return void_;
15686
+ }
15687
+ }));
15688
+ },
15689
+ CallExpression: (node) => {
15690
+ if (!isCallExpression$1(node) || !isFoldkitEvoCall(node, references) || isInsideFoldWrite(node, references)) return void_;
15691
+ const [parentModel, updates] = node.arguments;
15692
+ if (!isIdentifier(parentModel) || !isObjectExpression(updates)) return void_;
15693
+ return forEach(updates.properties, (property) => {
15694
+ if (!isObjectProperty(property) || property.computed || !isArrowFunction$1(property.value)) return void_;
15695
+ return pipe(all({
15696
+ childFieldName: staticPropertyName(property),
15697
+ result: resultBinding(property.value)
15698
+ }), flatMap$3(({ childFieldName, result }) => {
15699
+ const origin = references === void 0 ? fallbackOriginsByName.get(result.name) : getOrUndefined(map$5(resolvedVariable(references, result), (variable) => originsByVariable.get(variable)));
15700
+ return origin === void 0 ? none() : some$1({
15701
+ childFieldName,
15702
+ origin,
15703
+ result
15704
+ });
15705
+ }), filter$2(({ childFieldName, origin, result }) => childFieldName === origin.childFieldName && sameBinding(parentModel, origin.parentModel, references) && !sameBinding(result, origin.parentModel, references)), match$3({
15706
+ onNone: () => void_,
15707
+ onSome: ({ childFieldName, origin, result }) => ctx.report(Diagnostic_exports.make({
15708
+ node: property.value,
15709
+ message: manualChildResultMessage(childFieldName, origin.helperLabel, result.name)
15710
+ }))
15711
+ }));
15712
+ }, { discard: true });
15713
+ }
15714
+ };
15715
+ }
15716
+ });
15717
+ //#endregion
15102
15718
  //#region src/rules/require-rel-for-external-link.ts
15103
15719
  const protectiveRelSubstrings = ["noopener", "noreferrer"];
15104
15720
  const isTargetBlankElement = (element, references) => {
@@ -15130,7 +15746,7 @@ const requireRelForExternalLink = Rule_exports.define({
15130
15746
  const scopes = ctx.sourceCode.scopeManager?.scopes;
15131
15747
  const references = scopes === void 0 ? void 0 : indexReferences(scopes);
15132
15748
  return { ArrayExpression: (node) => {
15133
- if (!isArrayExpression(node)) return void_;
15749
+ if (!isArrayExpression$1(node)) return void_;
15134
15750
  if (some(node.elements, isSpreadElement)) return void_;
15135
15751
  const maybeTargetBlank = findFirst(node.elements, (element) => isTargetBlankElement(element, references));
15136
15752
  if (isNone(maybeTargetBlank)) return void_;
@@ -15399,6 +16015,7 @@ const basePlugin = Plugin_exports.define({
15399
16015
  "mount-factory-must-use-element": mountFactoryMustUseElement,
15400
16016
  "no-array-index-view-keys": noArrayIndexViewKeys,
15401
16017
  "no-child-message-construction-in-root": noChildMessageConstructionInRoot,
16018
+ "no-direct-submodel-state-update": noDirectSubmodelStateUpdate,
15402
16019
  "no-disabling-dev-guardrails": noDisablingDevGuardrails,
15403
16020
  "no-duplicate-onmount-per-element": noDuplicateOnmountPerElement,
15404
16021
  "no-empty-children-array": noEmptyChildrenArray,
@@ -15420,6 +16037,7 @@ const basePlugin = Plugin_exports.define({
15420
16037
  "prefer-command-mapmessage": preferCommandMapmessage,
15421
16038
  "prefer-effect-module-names": preferEffectModuleNames,
15422
16039
  "prefer-option-over-nullable-in-model": preferOptionOverNullableInModel,
16040
+ "require-fold-for-child-update-result": requireFoldForChildUpdateResult,
15423
16041
  "require-rel-for-external-link": requireRelForExternalLink,
15424
16042
  "selection-submodel-factory-at-module-scope": selectionSubmodelFactoryAtModuleScope,
15425
16043
  "wrap-child-output-in-got-message": wrapChildOutputInGotMessage
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Flags direct construction of a child Submodel Message variant from outside
3
- * the child, such as Chat.Message.ClickedOpen(...).
2
+ * Flags construction of a child Submodel Message variant from outside the
3
+ * child, such as Chat.Message.ClickedOpen(...) or an immutable local alias.
4
4
  */
5
5
  export declare const noChildMessageConstructionInRoot: import("effect-oxlint").CreateRule;
6
6
  //# sourceMappingURL=no-child-message-construction-in-root.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"no-child-message-construction-in-root.d.ts","sourceRoot":"","sources":["../../src/rules/no-child-message-construction-in-root.ts"],"names":[],"mappings":"AA4VA;;;GAGG;AACH,eAAO,MAAM,gCAAgC,oCA0C3C,CAAA"}
1
+ {"version":3,"file":"no-child-message-construction-in-root.d.ts","sourceRoot":"","sources":["../../src/rules/no-child-message-construction-in-root.ts"],"names":[],"mappings":"AA+YA;;;GAGG;AACH,eAAO,MAAM,gCAAgC,oCA0C3C,CAAA"}
@@ -0,0 +1,3 @@
1
+ /** Flags parent-owned evo calls that directly update a child Submodel Model. */
2
+ export declare const noDirectSubmodelStateUpdate: import("effect-oxlint").CreateRule;
3
+ //# sourceMappingURL=no-direct-submodel-state-update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-direct-submodel-state-update.d.ts","sourceRoot":"","sources":["../../src/rules/no-direct-submodel-state-update.ts"],"names":[],"mappings":"AAuyBA,gFAAgF;AAChF,eAAO,MAAM,2BAA2B,oCA+DtC,CAAA"}
@@ -0,0 +1,6 @@
1
+ /** Flags a parent that copies a namespaced child helper or update Return's Model
2
+ * into the matching evo field. It requires a matching Update.foldChild or
3
+ * Update.foldChildStep reference in the file, and leaves ordinary helpers,
4
+ * direct imports, aliases, init assembly, and Model-only reflect helpers alone. */
5
+ export declare const requireFoldForChildUpdateResult: import("effect-oxlint").CreateRule;
6
+ //# sourceMappingURL=require-fold-for-child-update-result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-fold-for-child-update-result.d.ts","sourceRoot":"","sources":["../../src/rules/require-fold-for-child-update-result.ts"],"names":[],"mappings":"AA+bA;;;oFAGoF;AACpF,eAAO,MAAM,+BAA+B,oCAiJ1C,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/oxlint-plugin",
3
- "version": "0.13.0-canary.41f95ced5e07",
3
+ "version": "0.13.0-canary.4cb3546e35b7",
4
4
  "description": "Oxlint rules for Foldkit applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,7 +53,8 @@
53
53
  "scripts": {
54
54
  "clean": "rimraf dist recommended.json all.json *.tsbuildinfo",
55
55
  "build": "pnpm run clean && rolldown -c rolldown.config.ts && tsc -b tsconfig.build.json && node scripts/generate-presets.mjs",
56
- "test": "vitest run",
56
+ "test": "pnpm run typecheck:real-oxlint-fixtures && vitest run",
57
+ "typecheck:real-oxlint-fixtures": "node scripts/typecheck-real-oxlint-fixtures.mjs",
57
58
  "typecheck": "tsc --noEmit"
58
59
  }
59
60
  }
package/recommended.json CHANGED
@@ -17,6 +17,7 @@
17
17
  "foldkit/mount-factory-must-use-element": "error",
18
18
  "foldkit/no-array-index-view-keys": "error",
19
19
  "foldkit/no-child-message-construction-in-root": "error",
20
+ "foldkit/no-direct-submodel-state-update": "error",
20
21
  "foldkit/no-disabling-dev-guardrails": "error",
21
22
  "foldkit/no-duplicate-onmount-per-element": "error",
22
23
  "foldkit/no-empty-children-array": "error",
@@ -38,6 +39,7 @@
38
39
  "foldkit/prefer-command-mapmessage": "error",
39
40
  "foldkit/prefer-effect-module-names": "error",
40
41
  "foldkit/prefer-option-over-nullable-in-model": "error",
42
+ "foldkit/require-fold-for-child-update-result": "error",
41
43
  "foldkit/require-rel-for-external-link": "error",
42
44
  "foldkit/selection-submodel-factory-at-module-scope": "error",
43
45
  "foldkit/wrap-child-output-in-got-message": "error"
@@ -100,6 +102,7 @@
100
102
  "foldkit/mount-factory-must-use-element": "off",
101
103
  "foldkit/no-array-index-view-keys": "off",
102
104
  "foldkit/no-child-message-construction-in-root": "off",
105
+ "foldkit/no-direct-submodel-state-update": "off",
103
106
  "foldkit/no-disabling-dev-guardrails": "off",
104
107
  "foldkit/no-duplicate-onmount-per-element": "off",
105
108
  "foldkit/no-empty-children-array": "off",
@@ -119,6 +122,7 @@
119
122
  "foldkit/prefer-callable-message-constructor": "off",
120
123
  "foldkit/prefer-command-mapmessage": "off",
121
124
  "foldkit/prefer-option-over-nullable-in-model": "off",
125
+ "foldkit/require-fold-for-child-update-result": "off",
122
126
  "foldkit/require-rel-for-external-link": "off",
123
127
  "foldkit/selection-submodel-factory-at-module-scope": "off",
124
128
  "foldkit/wrap-child-output-in-got-message": "off"