@violetflux/eslint-plugin-kerros 0.3.1 → 0.3.3

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/dist/index.cjs CHANGED
@@ -718,7 +718,7 @@ const objectEnumerationMethods = /* @__PURE__ */ new Set([
718
718
  "keys",
719
719
  "values"
720
720
  ]);
721
- /** Read the snapshot argument from a complete enumeration or serialization call. */
721
+ /** Read the tracked value argument from a broad enumeration or serialization call. */
722
722
  function getBroadArgument(node) {
723
723
  const [argument] = node.arguments;
724
724
  if (!argument || argument.type === "SpreadElement") return false;
@@ -733,48 +733,96 @@ const noBroadStoreAccess = createRule({
733
733
  name: "no-broad-store-access",
734
734
  meta: {
735
735
  type: "problem",
736
- docs: { description: "Prevent complete Store enumeration and serialization." },
737
- schema: [],
738
- messages: { broadAccess: "Do not enumerate or spread the complete Store snapshot." }
736
+ docs: { description: "Prevent broad enumeration and serialization of complete Store snapshots." },
737
+ schema: [{
738
+ type: "object",
739
+ properties: {
740
+ includeObjectFields: { type: "boolean" },
741
+ includeStoreModels: { type: "boolean" }
742
+ },
743
+ additionalProperties: false
744
+ }],
745
+ messages: {
746
+ broadAccess: "Do not enumerate, serialize, or spread a complete selector-free Store snapshot.",
747
+ broadObjectField: "Do not enumerate, serialize, or spread an object field from a selector-free Store snapshot."
748
+ }
739
749
  },
740
- defaultOptions: [],
741
- create(context) {
742
- const { getIdentifierSymbol, getType, isStoreHookCall } = createKerrosTypeTools(context);
750
+ defaultOptions: [{
751
+ includeObjectFields: false,
752
+ includeStoreModels: false
753
+ }],
754
+ create(context, [options]) {
755
+ const { getFactoryKind, getIdentifierSymbol, getType, isStoreHookCall } = createKerrosTypeTools(context);
743
756
  const origins = createReferenceOriginTracker(context.sourceCode.ast);
744
- /** Test whether an expression originates from a selector-free Store snapshot. */
745
- const isSnapshotDerived = (input, seen = /* @__PURE__ */ new Set()) => {
757
+ const storeModels = /* @__PURE__ */ new Set();
758
+ const pendingReports = [];
759
+ /** Test whether an expression belongs to a function registered as a createStore model. */
760
+ const isInsideStoreModel = (expression) => {
761
+ let node = expression.parent;
762
+ while (node) {
763
+ if (node.type === "FunctionDeclaration" && node.id) {
764
+ const symbol = getIdentifierSymbol(node.id);
765
+ return symbol ? storeModels.has(symbol) : false;
766
+ }
767
+ if ((node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") && node.parent.type === "VariableDeclarator" && node.parent.id.type === "Identifier") {
768
+ const symbol = getIdentifierSymbol(node.parent.id);
769
+ return symbol ? storeModels.has(symbol) : false;
770
+ }
771
+ node = node.parent;
772
+ }
773
+ return false;
774
+ };
775
+ /** Classify whether an expression is a complete Store snapshot or one object field from it. */
776
+ const readStoreOrigin = (input, seen = /* @__PURE__ */ new Set()) => {
746
777
  const node = unwrapExpression(input);
747
778
  if (node.type === "CallExpression") {
748
779
  const selector = node.arguments[0];
749
- return (node.arguments.length === 0 || node.arguments.length === 1 && selector?.type !== "SpreadElement" && (getType(selector).flags & typescript.default.TypeFlags.Undefined) !== 0) && isStoreHookCall(node);
780
+ return (node.arguments.length === 0 || node.arguments.length === 1 && selector?.type !== "SpreadElement" && (getType(selector).flags & typescript.default.TypeFlags.Undefined) !== 0) && isStoreHookCall(node) ? "snapshot" : void 0;
750
781
  }
751
- if (node.type === "AssignmentExpression") return isSnapshotDerived(node.right, seen);
752
- if (node.type === "MemberExpression") return isSnapshotDerived(node.object, seen);
753
- if (node.type !== "Identifier") return false;
782
+ if (node.type === "AssignmentExpression") return readStoreOrigin(node.right, seen);
783
+ if (node.type === "MemberExpression") return readStoreOrigin(node.object, seen) ? "objectField" : void 0;
784
+ if (node.type !== "Identifier") return;
754
785
  const symbol = getIdentifierSymbol(node);
755
- if (!symbol || seen.has(symbol)) return false;
786
+ if (!symbol || seen.has(symbol)) return;
756
787
  seen.add(symbol);
757
- const derived = origins.resolve(symbol, node).some((source) => isSnapshotDerived(source, seen));
788
+ let result;
789
+ for (const source of origins.resolve(symbol, node)) {
790
+ const origin = readStoreOrigin(source.expression, seen);
791
+ if (!origin) continue;
792
+ if (!source.objectField && origin === "snapshot") {
793
+ result = "snapshot";
794
+ break;
795
+ }
796
+ result = "objectField";
797
+ }
758
798
  seen.delete(symbol);
759
- return derived;
799
+ return result;
760
800
  };
761
801
  /** Report one operation that subscribes to every enumerable field. */
762
802
  const reportBroadAccess = (expression) => {
763
- if (isSnapshotDerived(expression)) context.report({
764
- node: expression,
803
+ const origin = readStoreOrigin(expression);
804
+ if (origin === "snapshot") pendingReports.push({
805
+ expression,
765
806
  messageId: "broadAccess"
766
807
  });
808
+ else if (origin === "objectField" && options.includeObjectFields) pendingReports.push({
809
+ expression,
810
+ messageId: "broadObjectField"
811
+ });
767
812
  };
768
813
  /** Track object-valued bindings destructured from a snapshot. */
769
- const recordObjectBindings = (pattern, source, write) => {
814
+ const recordObjectBindings = (pattern, source, write, objectField = false) => {
770
815
  if (pattern.type === "Identifier") {
771
816
  if ((getType(pattern).flags & typescript.default.TypeFlags.Object) === 0) return;
772
817
  const symbol = getIdentifierSymbol(pattern);
773
- if (symbol) origins.record(symbol, source, write);
818
+ if (symbol) origins.record(symbol, {
819
+ expression: source,
820
+ objectField
821
+ }, write);
774
822
  return;
775
823
  }
776
824
  if (pattern.type === "AssignmentPattern") {
777
- recordObjectBindings(pattern.left, source, write);
825
+ recordObjectBindings(pattern.left, source, write, objectField);
778
826
  return;
779
827
  }
780
828
  if (pattern.type === "RestElement") return;
@@ -782,12 +830,19 @@ const noBroadStoreAccess = createRule({
782
830
  const entries = pattern.type === "ObjectPattern" ? pattern.properties : pattern.elements;
783
831
  for (const entry of entries) {
784
832
  if (!entry) continue;
785
- if (entry.type === "Property") recordObjectBindings(entry.value, source, write);
786
- else recordObjectBindings(entry, source, write);
833
+ if (entry.type === "Property") recordObjectBindings(entry.value, source, write, true);
834
+ else recordObjectBindings(entry, source, write, true);
787
835
  }
788
836
  };
789
837
  return {
790
838
  CallExpression(node) {
839
+ if (getFactoryKind(node) === "createStore") {
840
+ const model = node.arguments[0];
841
+ if (model?.type === "Identifier") {
842
+ const symbol = getIdentifierSymbol(model);
843
+ if (symbol) storeModels.add(symbol);
844
+ }
845
+ }
791
846
  const argument = getBroadArgument(node);
792
847
  if (argument) reportBroadAccess(argument);
793
848
  },
@@ -798,7 +853,10 @@ const noBroadStoreAccess = createRule({
798
853
  if (!node.init) return;
799
854
  if (node.id.type === "Identifier") {
800
855
  const symbol = getIdentifierSymbol(node.id);
801
- if (symbol) origins.record(symbol, node.init, node);
856
+ if (symbol) origins.record(symbol, {
857
+ expression: node.init,
858
+ objectField: false
859
+ }, node);
802
860
  return;
803
861
  }
804
862
  if (node.id.type === "ObjectPattern" && node.id.properties.some((property) => property.type === "RestElement")) reportBroadAccess(node.init);
@@ -807,7 +865,19 @@ const noBroadStoreAccess = createRule({
807
865
  AssignmentExpression(node) {
808
866
  if (node.left.type !== "Identifier") return;
809
867
  const symbol = getIdentifierSymbol(node.left);
810
- if (symbol) origins.record(symbol, node.right, node);
868
+ if (symbol) origins.record(symbol, {
869
+ expression: node.right,
870
+ objectField: false
871
+ }, node);
872
+ },
873
+ "Program:exit"() {
874
+ for (const { expression, messageId } of pendingReports) {
875
+ if (!options.includeStoreModels && isInsideStoreModel(expression)) continue;
876
+ context.report({
877
+ node: expression,
878
+ messageId
879
+ });
880
+ }
811
881
  }
812
882
  };
813
883
  }
@@ -2471,7 +2541,7 @@ const rules = {
2471
2541
  const plugin = {
2472
2542
  meta: {
2473
2543
  name: "@violetflux/eslint-plugin-kerros",
2474
- version: "0.3.1"
2544
+ version: "0.3.3"
2475
2545
  },
2476
2546
  rules
2477
2547
  };
package/dist/index.mjs CHANGED
@@ -690,7 +690,7 @@ const objectEnumerationMethods = /* @__PURE__ */ new Set([
690
690
  "keys",
691
691
  "values"
692
692
  ]);
693
- /** Read the snapshot argument from a complete enumeration or serialization call. */
693
+ /** Read the tracked value argument from a broad enumeration or serialization call. */
694
694
  function getBroadArgument(node) {
695
695
  const [argument] = node.arguments;
696
696
  if (!argument || argument.type === "SpreadElement") return false;
@@ -705,48 +705,96 @@ const noBroadStoreAccess = createRule({
705
705
  name: "no-broad-store-access",
706
706
  meta: {
707
707
  type: "problem",
708
- docs: { description: "Prevent complete Store enumeration and serialization." },
709
- schema: [],
710
- messages: { broadAccess: "Do not enumerate or spread the complete Store snapshot." }
708
+ docs: { description: "Prevent broad enumeration and serialization of complete Store snapshots." },
709
+ schema: [{
710
+ type: "object",
711
+ properties: {
712
+ includeObjectFields: { type: "boolean" },
713
+ includeStoreModels: { type: "boolean" }
714
+ },
715
+ additionalProperties: false
716
+ }],
717
+ messages: {
718
+ broadAccess: "Do not enumerate, serialize, or spread a complete selector-free Store snapshot.",
719
+ broadObjectField: "Do not enumerate, serialize, or spread an object field from a selector-free Store snapshot."
720
+ }
711
721
  },
712
- defaultOptions: [],
713
- create(context) {
714
- const { getIdentifierSymbol, getType, isStoreHookCall } = createKerrosTypeTools(context);
722
+ defaultOptions: [{
723
+ includeObjectFields: false,
724
+ includeStoreModels: false
725
+ }],
726
+ create(context, [options]) {
727
+ const { getFactoryKind, getIdentifierSymbol, getType, isStoreHookCall } = createKerrosTypeTools(context);
715
728
  const origins = createReferenceOriginTracker(context.sourceCode.ast);
716
- /** Test whether an expression originates from a selector-free Store snapshot. */
717
- const isSnapshotDerived = (input, seen = /* @__PURE__ */ new Set()) => {
729
+ const storeModels = /* @__PURE__ */ new Set();
730
+ const pendingReports = [];
731
+ /** Test whether an expression belongs to a function registered as a createStore model. */
732
+ const isInsideStoreModel = (expression) => {
733
+ let node = expression.parent;
734
+ while (node) {
735
+ if (node.type === "FunctionDeclaration" && node.id) {
736
+ const symbol = getIdentifierSymbol(node.id);
737
+ return symbol ? storeModels.has(symbol) : false;
738
+ }
739
+ if ((node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") && node.parent.type === "VariableDeclarator" && node.parent.id.type === "Identifier") {
740
+ const symbol = getIdentifierSymbol(node.parent.id);
741
+ return symbol ? storeModels.has(symbol) : false;
742
+ }
743
+ node = node.parent;
744
+ }
745
+ return false;
746
+ };
747
+ /** Classify whether an expression is a complete Store snapshot or one object field from it. */
748
+ const readStoreOrigin = (input, seen = /* @__PURE__ */ new Set()) => {
718
749
  const node = unwrapExpression(input);
719
750
  if (node.type === "CallExpression") {
720
751
  const selector = node.arguments[0];
721
- return (node.arguments.length === 0 || node.arguments.length === 1 && selector?.type !== "SpreadElement" && (getType(selector).flags & ts.TypeFlags.Undefined) !== 0) && isStoreHookCall(node);
752
+ return (node.arguments.length === 0 || node.arguments.length === 1 && selector?.type !== "SpreadElement" && (getType(selector).flags & ts.TypeFlags.Undefined) !== 0) && isStoreHookCall(node) ? "snapshot" : void 0;
722
753
  }
723
- if (node.type === "AssignmentExpression") return isSnapshotDerived(node.right, seen);
724
- if (node.type === "MemberExpression") return isSnapshotDerived(node.object, seen);
725
- if (node.type !== "Identifier") return false;
754
+ if (node.type === "AssignmentExpression") return readStoreOrigin(node.right, seen);
755
+ if (node.type === "MemberExpression") return readStoreOrigin(node.object, seen) ? "objectField" : void 0;
756
+ if (node.type !== "Identifier") return;
726
757
  const symbol = getIdentifierSymbol(node);
727
- if (!symbol || seen.has(symbol)) return false;
758
+ if (!symbol || seen.has(symbol)) return;
728
759
  seen.add(symbol);
729
- const derived = origins.resolve(symbol, node).some((source) => isSnapshotDerived(source, seen));
760
+ let result;
761
+ for (const source of origins.resolve(symbol, node)) {
762
+ const origin = readStoreOrigin(source.expression, seen);
763
+ if (!origin) continue;
764
+ if (!source.objectField && origin === "snapshot") {
765
+ result = "snapshot";
766
+ break;
767
+ }
768
+ result = "objectField";
769
+ }
730
770
  seen.delete(symbol);
731
- return derived;
771
+ return result;
732
772
  };
733
773
  /** Report one operation that subscribes to every enumerable field. */
734
774
  const reportBroadAccess = (expression) => {
735
- if (isSnapshotDerived(expression)) context.report({
736
- node: expression,
775
+ const origin = readStoreOrigin(expression);
776
+ if (origin === "snapshot") pendingReports.push({
777
+ expression,
737
778
  messageId: "broadAccess"
738
779
  });
780
+ else if (origin === "objectField" && options.includeObjectFields) pendingReports.push({
781
+ expression,
782
+ messageId: "broadObjectField"
783
+ });
739
784
  };
740
785
  /** Track object-valued bindings destructured from a snapshot. */
741
- const recordObjectBindings = (pattern, source, write) => {
786
+ const recordObjectBindings = (pattern, source, write, objectField = false) => {
742
787
  if (pattern.type === "Identifier") {
743
788
  if ((getType(pattern).flags & ts.TypeFlags.Object) === 0) return;
744
789
  const symbol = getIdentifierSymbol(pattern);
745
- if (symbol) origins.record(symbol, source, write);
790
+ if (symbol) origins.record(symbol, {
791
+ expression: source,
792
+ objectField
793
+ }, write);
746
794
  return;
747
795
  }
748
796
  if (pattern.type === "AssignmentPattern") {
749
- recordObjectBindings(pattern.left, source, write);
797
+ recordObjectBindings(pattern.left, source, write, objectField);
750
798
  return;
751
799
  }
752
800
  if (pattern.type === "RestElement") return;
@@ -754,12 +802,19 @@ const noBroadStoreAccess = createRule({
754
802
  const entries = pattern.type === "ObjectPattern" ? pattern.properties : pattern.elements;
755
803
  for (const entry of entries) {
756
804
  if (!entry) continue;
757
- if (entry.type === "Property") recordObjectBindings(entry.value, source, write);
758
- else recordObjectBindings(entry, source, write);
805
+ if (entry.type === "Property") recordObjectBindings(entry.value, source, write, true);
806
+ else recordObjectBindings(entry, source, write, true);
759
807
  }
760
808
  };
761
809
  return {
762
810
  CallExpression(node) {
811
+ if (getFactoryKind(node) === "createStore") {
812
+ const model = node.arguments[0];
813
+ if (model?.type === "Identifier") {
814
+ const symbol = getIdentifierSymbol(model);
815
+ if (symbol) storeModels.add(symbol);
816
+ }
817
+ }
763
818
  const argument = getBroadArgument(node);
764
819
  if (argument) reportBroadAccess(argument);
765
820
  },
@@ -770,7 +825,10 @@ const noBroadStoreAccess = createRule({
770
825
  if (!node.init) return;
771
826
  if (node.id.type === "Identifier") {
772
827
  const symbol = getIdentifierSymbol(node.id);
773
- if (symbol) origins.record(symbol, node.init, node);
828
+ if (symbol) origins.record(symbol, {
829
+ expression: node.init,
830
+ objectField: false
831
+ }, node);
774
832
  return;
775
833
  }
776
834
  if (node.id.type === "ObjectPattern" && node.id.properties.some((property) => property.type === "RestElement")) reportBroadAccess(node.init);
@@ -779,7 +837,19 @@ const noBroadStoreAccess = createRule({
779
837
  AssignmentExpression(node) {
780
838
  if (node.left.type !== "Identifier") return;
781
839
  const symbol = getIdentifierSymbol(node.left);
782
- if (symbol) origins.record(symbol, node.right, node);
840
+ if (symbol) origins.record(symbol, {
841
+ expression: node.right,
842
+ objectField: false
843
+ }, node);
844
+ },
845
+ "Program:exit"() {
846
+ for (const { expression, messageId } of pendingReports) {
847
+ if (!options.includeStoreModels && isInsideStoreModel(expression)) continue;
848
+ context.report({
849
+ node: expression,
850
+ messageId
851
+ });
852
+ }
783
853
  }
784
854
  };
785
855
  }
@@ -2443,7 +2513,7 @@ const rules = {
2443
2513
  const plugin = {
2444
2514
  meta: {
2445
2515
  name: "@violetflux/eslint-plugin-kerros",
2446
- version: "0.3.1"
2516
+ version: "0.3.3"
2447
2517
  },
2448
2518
  rules
2449
2519
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@violetflux/eslint-plugin-kerros",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Type-aware ESLint rules for Kerros stores.",
5
5
  "repository": {
6
6
  "type": "git",