@sarj/eslint-plugin 18.2.0 → 18.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1824,19 +1824,19 @@ function firstParameterBindings(source, node) {
1824
1824
  function collectParameterBindings(source, node) {
1825
1825
  if (hasDirectEval(source)) return /* @__PURE__ */ new Set();
1826
1826
  const parameter = node.params[0];
1827
- const identifiers = [];
1827
+ const identifiers = /* @__PURE__ */ new Set();
1828
1828
  if (parameter?.type === import_utils14.AST_NODE_TYPES.Identifier)
1829
- identifiers.push(parameter);
1829
+ identifiers.add(parameter);
1830
1830
  if (parameter?.type === import_utils14.AST_NODE_TYPES.ObjectPattern) {
1831
1831
  for (const property of parameter.properties) {
1832
1832
  if (property.type === import_utils14.AST_NODE_TYPES.Property && !property.computed && property.value.type === import_utils14.AST_NODE_TYPES.Identifier)
1833
- identifiers.push(property.value);
1833
+ identifiers.add(property.value);
1834
1834
  }
1835
1835
  }
1836
1836
  return new Set(
1837
1837
  source.getDeclaredVariables(node).filter(
1838
- (variable) => identifiers.some(
1839
- (identifier) => variable.identifiers.includes(identifier)
1838
+ (variable) => variable.identifiers.some(
1839
+ (identifier) => identifiers.has(identifier)
1840
1840
  ) && stableValueBinding(variable)
1841
1841
  )
1842
1842
  );
@@ -1879,8 +1879,10 @@ function reactHook(source, callee, hook) {
1879
1879
  let identifier;
1880
1880
  let namespace = false;
1881
1881
  if (callee.type === import_utils14.AST_NODE_TYPES.Identifier) identifier = callee;
1882
- else if (callee.type === import_utils14.AST_NODE_TYPES.MemberExpression && !callee.optional && import_utils14.ASTUtils.getPropertyName(callee) === hook && callee.object.type === import_utils14.AST_NODE_TYPES.Identifier) {
1883
- identifier = callee.object;
1882
+ else if (callee.type === import_utils14.AST_NODE_TYPES.MemberExpression && !callee.optional && import_utils14.ASTUtils.getPropertyName(callee) === hook) {
1883
+ const object = unwrapExpression(callee.object);
1884
+ if (object.type !== import_utils14.AST_NODE_TYPES.Identifier) return false;
1885
+ identifier = object;
1884
1886
  namespace = true;
1885
1887
  } else return false;
1886
1888
  const variable = import_utils14.ASTUtils.findVariable(
@@ -1888,17 +1890,33 @@ function reactHook(source, callee, hook) {
1888
1890
  identifier.name
1889
1891
  );
1890
1892
  if (variable === null) return false;
1891
- const specifier = reactImportSpecifier(variable);
1892
- if (specifier === null) return false;
1893
- return namespace ? specifier.type === import_utils14.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils14.AST_NODE_TYPES.ImportNamespaceSpecifier : specifier.type === import_utils14.AST_NODE_TYPES.ImportSpecifier && specifier.importKind !== "type" && importSpecifierName(specifier) === hook;
1894
- }
1895
- function reactImportSpecifier(variable) {
1896
- let specifier = REACT_IMPORTS.get(variable);
1897
- if (specifier !== void 0) return specifier;
1893
+ const imported = reactImport(variable);
1894
+ if (imported === null) return false;
1895
+ const { specifier, replacedMembers } = imported;
1896
+ return namespace ? (specifier.type === import_utils14.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils14.AST_NODE_TYPES.ImportNamespaceSpecifier) && replacedMembers?.has(null) !== true && replacedMembers?.has(hook) !== true : specifier.type === import_utils14.AST_NODE_TYPES.ImportSpecifier && specifier.importKind !== "type" && importSpecifierName(specifier) === hook;
1897
+ }
1898
+ function reactImport(variable) {
1899
+ let imported = REACT_IMPORTS.get(variable);
1900
+ if (imported !== void 0) return imported;
1898
1901
  const definition = variable.defs[0];
1899
- specifier = variable.defs.length === 1 && definition?.type === "ImportBinding" && definition.parent.type === import_utils14.AST_NODE_TYPES.ImportDeclaration && definition.parent.source.value === "react" && definition.parent.importKind !== "type" && stableValueBinding(variable) ? definition.node : null;
1900
- REACT_IMPORTS.set(variable, specifier);
1901
- return specifier;
1902
+ imported = null;
1903
+ if (variable.defs.length === 1 && definition?.type === "ImportBinding" && definition.parent.type === import_utils14.AST_NODE_TYPES.ImportDeclaration && definition.parent.source.value === "react" && definition.parent.importKind !== "type") {
1904
+ const specifier = definition.node;
1905
+ const replacedMembers = specifier.type === import_utils14.AST_NODE_TYPES.ImportSpecifier ? void 0 : /* @__PURE__ */ new Set();
1906
+ if (stableValueBinding(variable, replacedMembers === void 0 ? void 0 : (reference) => {
1907
+ const object = outerExpression(reference.identifier);
1908
+ const member3 = object.parent;
1909
+ if (member3?.type !== import_utils14.AST_NODE_TYPES.MemberExpression || member3.object !== object)
1910
+ return true;
1911
+ const target = outerExpression(member3);
1912
+ if (target.parent?.type === import_utils14.AST_NODE_TYPES.MemberExpression && target.parent.object === target)
1913
+ return true;
1914
+ if (writtenMember(member3)) replacedMembers.add(import_utils14.ASTUtils.getPropertyName(member3));
1915
+ return true;
1916
+ })) imported = { specifier, replacedMembers };
1917
+ }
1918
+ REACT_IMPORTS.set(variable, imported);
1919
+ return imported;
1902
1920
  }
1903
1921
  function containsView(node) {
1904
1922
  if (node.type === import_utils14.AST_NODE_TYPES.JSXElement || node.type === import_utils14.AST_NODE_TYPES.JSXFragment)
@@ -1953,6 +1971,7 @@ var PREFER_SERVER_DATA_PREPARATION_DOCUMENTATION = {
1953
1971
  "Advisory only. Local syntax cannot establish whether a caller is a Server Component, whether props change interactively or whether server preparation reduces the total cost. This rule is a review lead, never proof that code can be moved to the server. No automatic fix is provided.",
1954
1972
  "Requires an explicit use client directive and parser-owned TypeScript services resolving map/filter/flatMap to the standard Array or ReadonlyArray library signature. The receiver beneath erased wrappers must itself have an Array, ReadonlyArray or tuple type after nullish narrowing; casts from opaque or custom collections do not establish this. Subclass and structural collection types outside those native array types are excluded. The containing view must have an independent React useState/useReducer call or a native callable type on an intrinsic DOM onClick/onChange/onInput/onSubmit handler. Nullish, noncallable and opaque handler types do not establish interaction. Noninteractive boundaries remain owned by no-unnecessary-use-client; custom hooks and indirect client requirements are unsupported.",
1955
1973
  "Every runtime reference to the source parameter binding must be a supported native data projection or a field-only JSX display loop. Inputs also needed for editing, whole-record forwarding, lookup, mutation, other fields or opaque helpers are excluded. Files containing direct eval are excluded because it can observe or change lexical bindings; indirect eval and properties named eval do not establish lexical access. Only direct first-parameter paths and synchronous single-expression callbacks are supported. Whole-row identity maps and constant maps are excluded; supported projections must read a fixed input field.",
1974
+ "Field-only object and dense array literals can form nested data shapes, including readonly tuples and flatMap outputs. Array holes, spreads, captured values and JSX outputs remain unsupported. Hook recognition follows erased namespace wrappers and excludes direct writes to the requested imported member; reflective or external API patches are not modeled.",
1956
1975
  "JSX-rendering callbacks are never reported as preparation findings. Callback calls, captured values, destructuring defaults/rest/nesting, async callbacks, event handlers, effects, memo callbacks, opaque helpers, dynamic/optional paths, custom collection signatures, missing type services, generated source and tests are excluded. First-parameter provenance is syntactic; annotations cannot prove absence of runtime getters or patched array methods. Analysis does not claim callback purity or preserve semantics of a hypothetical move.",
1957
1976
  "Reports the direct source projection once; chained operations with a call receiver are not independently reported. A static subprojection may still be prepared by its producer while a union with pending local edits remains in the client."
1958
1977
  ],
@@ -2107,6 +2126,10 @@ function projection(source, callback, mode = "data") {
2107
2126
  return inspect(node.left, depth + 1) && inspect(node.right, depth + 1);
2108
2127
  if (node.type === import_utils15.AST_NODE_TYPES.ConditionalExpression)
2109
2128
  return inspect(node.test, depth + 1) && inspect(node.consequent, depth + 1) && inspect(node.alternate, depth + 1);
2129
+ if (node.type === import_utils15.AST_NODE_TYPES.ArrayExpression)
2130
+ return node.elements.every(
2131
+ (element) => element !== null && element.type !== import_utils15.AST_NODE_TYPES.SpreadElement && inspect(element, depth + 1)
2132
+ );
2110
2133
  if (node.type === import_utils15.AST_NODE_TYPES.ObjectExpression)
2111
2134
  return node.properties.every(
2112
2135
  (property) => property.type === import_utils15.AST_NODE_TYPES.Property && property.kind === "init" && !property.method && !property.computed && inspect(property.value, depth + 1)
@@ -2191,8 +2214,9 @@ var NO_READONLY_PROP_STATE_DOCUMENTATION = {
2191
2214
  category: "correctness",
2192
2215
  limitations: [
2193
2216
  "Advisory only: initial snapshots can be intentional. There is no automatic fix because replacing state with current props changes how later updates behave.",
2194
- "Recognizes direct React useState imports and default/namespace member calls, including renamed imports, through lexical binding identity. Checks a direct array destructure in a function containing its own JSX and a direct first-parameter property path, or a zero-argument synchronous lazy initializer returning that path.",
2217
+ "Recognizes direct React useState imports and default/namespace member calls, including renamed imports and erased wrappers around imported namespaces, through lexical binding identity. Checks a direct array destructure in a function containing its own JSX and a direct first-parameter property path, or a zero-argument synchronous lazy initializer returning that path.",
2195
2218
  "Eager initializers require parser-owned TypeScript services and primitive types, including unions composed entirely of primitives. Object and void types can hide callable values through structural assignment or ignored return values, so eager nonprimitive values, Any, unknown and unresolved generics are excluded. React executes function arguments as lazy initializers. Explicit zero-argument lazy callbacks returning a prop directly remain supported for all value types without type services. Opaque casts cannot prove runtime identity; lifecycle changes stay manual.",
2219
+ "Direct writes to the requested imported namespace member exclude its hook calls. Unknown computed writes exclude all namespace hooks; unrelated member writes and a hook function's own properties do not replace the callable. Reflective mutation and external API patches are not modeled.",
2196
2220
  "An updater read, escape, reassignment or nontrivial destructuring excludes the finding. Unused state values remain owned by unused-variable checks. Files containing direct eval are excluded because it can observe or change lexical bindings. Indirect eval and properties named eval do not establish lexical access. Opaque initialization, property aliases, optional/dynamic paths, rest/default bindings, non-JSX hooks, generated source and tests are outside scope. This does not ban interactive useState or replace the upstream set-state-in-effect rule."
2197
2221
  ],
2198
2222
  references: [
@@ -2481,18 +2505,8 @@ var FRONTEND_IMPORT = /^(?:react(?:-dom)?|next|preact|vue|svelte|@angular\/core|
2481
2505
  function isFrontendSource(context) {
2482
2506
  return /\.[jt]sx$/u.test(context.filename) || frontendModule(context.sourceCode.ast) || frontendPackage(context.filename);
2483
2507
  }
2484
- var FRONTEND_PACKAGE_BY_DIRECTORY = /* @__PURE__ */ new Map();
2485
2508
  function frontendPackage(filename) {
2486
- const directory = (0, import_node_path2.dirname)((0, import_node_path2.resolve)(filename));
2487
- let cached = FRONTEND_PACKAGE_BY_DIRECTORY.get(directory);
2488
- if (cached === void 0) {
2489
- cached = nearestManifestDeclaresFrontend(directory);
2490
- FRONTEND_PACKAGE_BY_DIRECTORY.set(directory, cached);
2491
- }
2492
- return cached;
2493
- }
2494
- function nearestManifestDeclaresFrontend(start) {
2495
- let directory = start;
2509
+ let directory = (0, import_node_path2.dirname)((0, import_node_path2.resolve)(filename));
2496
2510
  for (; ; ) {
2497
2511
  const manifestPath = (0, import_node_path2.join)(directory, "package.json");
2498
2512
  if ((0, import_node_fs.existsSync)(manifestPath)) {
@@ -2838,6 +2852,23 @@ function ownsTestComments(context) {
2838
2852
  const framework = context.sourceCode.ast.body.some((statement) => statement.type === import_utils22.AST_NODE_TYPES.ImportDeclaration && statement.importKind !== "type" && (statement.specifiers.length === 0 || statement.specifiers.some((specifier) => specifier.type !== import_utils22.AST_NODE_TYPES.ImportSpecifier || specifier.importKind !== "type")) && ["vitest", "@jest/globals", "@playwright/test", "bun:test", "node:test"].includes(statement.source.value));
2839
2853
  return unambiguous || framework;
2840
2854
  }
2855
+ function legalNoticeComments(comments) {
2856
+ const legal = /* @__PURE__ */ new Set();
2857
+ for (const [index, comment] of comments.entries()) {
2858
+ if (legal.has(comment) || comment.type !== "Line" || !LEGAL.test(comment.value)) continue;
2859
+ legal.add(comment);
2860
+ for (const direction of [-1, 1]) {
2861
+ let previous = comment;
2862
+ for (let next = index + direction; next >= 0 && next < comments.length; next += direction) {
2863
+ const neighbor = comments[next];
2864
+ if (neighbor?.type !== "Line" || Math.abs(neighbor.loc.start.line - previous.loc.start.line) !== 1) break;
2865
+ legal.add(neighbor);
2866
+ previous = neighbor;
2867
+ }
2868
+ }
2869
+ }
2870
+ return legal;
2871
+ }
2841
2872
  var no_test_comments_default = createRule({
2842
2873
  name: "no-test-comments",
2843
2874
  documentation: NO_TEST_COMMENTS_DOCUMENTATION,
@@ -2856,23 +2887,10 @@ var no_test_comments_default = createRule({
2856
2887
  return {
2857
2888
  Program() {
2858
2889
  const comments = context.sourceCode.getAllComments();
2859
- const legalLines = /* @__PURE__ */ new Set();
2860
- for (const [index, comment] of comments.entries()) {
2861
- if (comment.type !== "Line" || !LEGAL.test(comment.value)) continue;
2862
- legalLines.add(index);
2863
- for (const direction of [-1, 1]) {
2864
- let previous = comment;
2865
- for (let next = index + direction; next >= 0 && next < comments.length; next += direction) {
2866
- const neighbor = comments[next];
2867
- if (neighbor?.type !== "Line" || Math.abs(neighbor.loc.start.line - previous.loc.start.line) !== 1) break;
2868
- legalLines.add(next);
2869
- previous = neighbor;
2870
- }
2871
- }
2872
- }
2873
- for (const [index, comment] of comments.entries()) {
2890
+ const legal = frontend ? void 0 : legalNoticeComments(comments);
2891
+ for (const comment of comments) {
2874
2892
  const body3 = comment.value.trim().replace(/^\*\s*/u, "");
2875
- if (!frontend && (DIRECTIVE.test(body3) || BEHAVIORAL_DIRECTIVE.test(comment.value) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value) || hasDetectSecretsPragma(comment, context.sourceCode.text))) continue;
2893
+ if (!frontend && (DIRECTIVE.test(body3) || BEHAVIORAL_DIRECTIVE.test(comment.value) || LEGAL.test(comment.value) || legal?.has(comment) || TYPE_CONTRACT.test(comment.value) || hasDetectSecretsPragma(comment, context.sourceCode.text))) continue;
2876
2894
  context.report({
2877
2895
  node: comment,
2878
2896
  messageId: "testComment",
@@ -6287,7 +6305,7 @@ function isSearchParamReceiver(call, services) {
6287
6305
  // package.json
6288
6306
  var package_default = {
6289
6307
  name: "@sarj/eslint-plugin",
6290
- version: "18.2.0",
6308
+ version: "18.2.1",
6291
6309
  packageManager: "npm@12.2.1",
6292
6310
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6293
6311
  type: "module",
package/dist/index.js CHANGED
@@ -1782,19 +1782,19 @@ function firstParameterBindings(source, node) {
1782
1782
  function collectParameterBindings(source, node) {
1783
1783
  if (hasDirectEval(source)) return /* @__PURE__ */ new Set();
1784
1784
  const parameter = node.params[0];
1785
- const identifiers = [];
1785
+ const identifiers = /* @__PURE__ */ new Set();
1786
1786
  if (parameter?.type === AST_NODE_TYPES13.Identifier)
1787
- identifiers.push(parameter);
1787
+ identifiers.add(parameter);
1788
1788
  if (parameter?.type === AST_NODE_TYPES13.ObjectPattern) {
1789
1789
  for (const property of parameter.properties) {
1790
1790
  if (property.type === AST_NODE_TYPES13.Property && !property.computed && property.value.type === AST_NODE_TYPES13.Identifier)
1791
- identifiers.push(property.value);
1791
+ identifiers.add(property.value);
1792
1792
  }
1793
1793
  }
1794
1794
  return new Set(
1795
1795
  source.getDeclaredVariables(node).filter(
1796
- (variable) => identifiers.some(
1797
- (identifier) => variable.identifiers.includes(identifier)
1796
+ (variable) => variable.identifiers.some(
1797
+ (identifier) => identifiers.has(identifier)
1798
1798
  ) && stableValueBinding(variable)
1799
1799
  )
1800
1800
  );
@@ -1837,8 +1837,10 @@ function reactHook(source, callee, hook) {
1837
1837
  let identifier;
1838
1838
  let namespace = false;
1839
1839
  if (callee.type === AST_NODE_TYPES13.Identifier) identifier = callee;
1840
- else if (callee.type === AST_NODE_TYPES13.MemberExpression && !callee.optional && ASTUtils6.getPropertyName(callee) === hook && callee.object.type === AST_NODE_TYPES13.Identifier) {
1841
- identifier = callee.object;
1840
+ else if (callee.type === AST_NODE_TYPES13.MemberExpression && !callee.optional && ASTUtils6.getPropertyName(callee) === hook) {
1841
+ const object = unwrapExpression(callee.object);
1842
+ if (object.type !== AST_NODE_TYPES13.Identifier) return false;
1843
+ identifier = object;
1842
1844
  namespace = true;
1843
1845
  } else return false;
1844
1846
  const variable = ASTUtils6.findVariable(
@@ -1846,17 +1848,33 @@ function reactHook(source, callee, hook) {
1846
1848
  identifier.name
1847
1849
  );
1848
1850
  if (variable === null) return false;
1849
- const specifier = reactImportSpecifier(variable);
1850
- if (specifier === null) return false;
1851
- return namespace ? specifier.type === AST_NODE_TYPES13.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES13.ImportNamespaceSpecifier : specifier.type === AST_NODE_TYPES13.ImportSpecifier && specifier.importKind !== "type" && importSpecifierName(specifier) === hook;
1852
- }
1853
- function reactImportSpecifier(variable) {
1854
- let specifier = REACT_IMPORTS.get(variable);
1855
- if (specifier !== void 0) return specifier;
1851
+ const imported = reactImport(variable);
1852
+ if (imported === null) return false;
1853
+ const { specifier, replacedMembers } = imported;
1854
+ return namespace ? (specifier.type === AST_NODE_TYPES13.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES13.ImportNamespaceSpecifier) && replacedMembers?.has(null) !== true && replacedMembers?.has(hook) !== true : specifier.type === AST_NODE_TYPES13.ImportSpecifier && specifier.importKind !== "type" && importSpecifierName(specifier) === hook;
1855
+ }
1856
+ function reactImport(variable) {
1857
+ let imported = REACT_IMPORTS.get(variable);
1858
+ if (imported !== void 0) return imported;
1856
1859
  const definition = variable.defs[0];
1857
- specifier = variable.defs.length === 1 && definition?.type === "ImportBinding" && definition.parent.type === AST_NODE_TYPES13.ImportDeclaration && definition.parent.source.value === "react" && definition.parent.importKind !== "type" && stableValueBinding(variable) ? definition.node : null;
1858
- REACT_IMPORTS.set(variable, specifier);
1859
- return specifier;
1860
+ imported = null;
1861
+ if (variable.defs.length === 1 && definition?.type === "ImportBinding" && definition.parent.type === AST_NODE_TYPES13.ImportDeclaration && definition.parent.source.value === "react" && definition.parent.importKind !== "type") {
1862
+ const specifier = definition.node;
1863
+ const replacedMembers = specifier.type === AST_NODE_TYPES13.ImportSpecifier ? void 0 : /* @__PURE__ */ new Set();
1864
+ if (stableValueBinding(variable, replacedMembers === void 0 ? void 0 : (reference) => {
1865
+ const object = outerExpression(reference.identifier);
1866
+ const member3 = object.parent;
1867
+ if (member3?.type !== AST_NODE_TYPES13.MemberExpression || member3.object !== object)
1868
+ return true;
1869
+ const target = outerExpression(member3);
1870
+ if (target.parent?.type === AST_NODE_TYPES13.MemberExpression && target.parent.object === target)
1871
+ return true;
1872
+ if (writtenMember(member3)) replacedMembers.add(ASTUtils6.getPropertyName(member3));
1873
+ return true;
1874
+ })) imported = { specifier, replacedMembers };
1875
+ }
1876
+ REACT_IMPORTS.set(variable, imported);
1877
+ return imported;
1860
1878
  }
1861
1879
  function containsView(node) {
1862
1880
  if (node.type === AST_NODE_TYPES13.JSXElement || node.type === AST_NODE_TYPES13.JSXFragment)
@@ -1911,6 +1929,7 @@ var PREFER_SERVER_DATA_PREPARATION_DOCUMENTATION = {
1911
1929
  "Advisory only. Local syntax cannot establish whether a caller is a Server Component, whether props change interactively or whether server preparation reduces the total cost. This rule is a review lead, never proof that code can be moved to the server. No automatic fix is provided.",
1912
1930
  "Requires an explicit use client directive and parser-owned TypeScript services resolving map/filter/flatMap to the standard Array or ReadonlyArray library signature. The receiver beneath erased wrappers must itself have an Array, ReadonlyArray or tuple type after nullish narrowing; casts from opaque or custom collections do not establish this. Subclass and structural collection types outside those native array types are excluded. The containing view must have an independent React useState/useReducer call or a native callable type on an intrinsic DOM onClick/onChange/onInput/onSubmit handler. Nullish, noncallable and opaque handler types do not establish interaction. Noninteractive boundaries remain owned by no-unnecessary-use-client; custom hooks and indirect client requirements are unsupported.",
1913
1931
  "Every runtime reference to the source parameter binding must be a supported native data projection or a field-only JSX display loop. Inputs also needed for editing, whole-record forwarding, lookup, mutation, other fields or opaque helpers are excluded. Files containing direct eval are excluded because it can observe or change lexical bindings; indirect eval and properties named eval do not establish lexical access. Only direct first-parameter paths and synchronous single-expression callbacks are supported. Whole-row identity maps and constant maps are excluded; supported projections must read a fixed input field.",
1932
+ "Field-only object and dense array literals can form nested data shapes, including readonly tuples and flatMap outputs. Array holes, spreads, captured values and JSX outputs remain unsupported. Hook recognition follows erased namespace wrappers and excludes direct writes to the requested imported member; reflective or external API patches are not modeled.",
1914
1933
  "JSX-rendering callbacks are never reported as preparation findings. Callback calls, captured values, destructuring defaults/rest/nesting, async callbacks, event handlers, effects, memo callbacks, opaque helpers, dynamic/optional paths, custom collection signatures, missing type services, generated source and tests are excluded. First-parameter provenance is syntactic; annotations cannot prove absence of runtime getters or patched array methods. Analysis does not claim callback purity or preserve semantics of a hypothetical move.",
1915
1934
  "Reports the direct source projection once; chained operations with a call receiver are not independently reported. A static subprojection may still be prepared by its producer while a union with pending local edits remains in the client."
1916
1935
  ],
@@ -2065,6 +2084,10 @@ function projection(source, callback, mode = "data") {
2065
2084
  return inspect(node.left, depth + 1) && inspect(node.right, depth + 1);
2066
2085
  if (node.type === AST_NODE_TYPES14.ConditionalExpression)
2067
2086
  return inspect(node.test, depth + 1) && inspect(node.consequent, depth + 1) && inspect(node.alternate, depth + 1);
2087
+ if (node.type === AST_NODE_TYPES14.ArrayExpression)
2088
+ return node.elements.every(
2089
+ (element) => element !== null && element.type !== AST_NODE_TYPES14.SpreadElement && inspect(element, depth + 1)
2090
+ );
2068
2091
  if (node.type === AST_NODE_TYPES14.ObjectExpression)
2069
2092
  return node.properties.every(
2070
2093
  (property) => property.type === AST_NODE_TYPES14.Property && property.kind === "init" && !property.method && !property.computed && inspect(property.value, depth + 1)
@@ -2149,8 +2172,9 @@ var NO_READONLY_PROP_STATE_DOCUMENTATION = {
2149
2172
  category: "correctness",
2150
2173
  limitations: [
2151
2174
  "Advisory only: initial snapshots can be intentional. There is no automatic fix because replacing state with current props changes how later updates behave.",
2152
- "Recognizes direct React useState imports and default/namespace member calls, including renamed imports, through lexical binding identity. Checks a direct array destructure in a function containing its own JSX and a direct first-parameter property path, or a zero-argument synchronous lazy initializer returning that path.",
2175
+ "Recognizes direct React useState imports and default/namespace member calls, including renamed imports and erased wrappers around imported namespaces, through lexical binding identity. Checks a direct array destructure in a function containing its own JSX and a direct first-parameter property path, or a zero-argument synchronous lazy initializer returning that path.",
2153
2176
  "Eager initializers require parser-owned TypeScript services and primitive types, including unions composed entirely of primitives. Object and void types can hide callable values through structural assignment or ignored return values, so eager nonprimitive values, Any, unknown and unresolved generics are excluded. React executes function arguments as lazy initializers. Explicit zero-argument lazy callbacks returning a prop directly remain supported for all value types without type services. Opaque casts cannot prove runtime identity; lifecycle changes stay manual.",
2177
+ "Direct writes to the requested imported namespace member exclude its hook calls. Unknown computed writes exclude all namespace hooks; unrelated member writes and a hook function's own properties do not replace the callable. Reflective mutation and external API patches are not modeled.",
2154
2178
  "An updater read, escape, reassignment or nontrivial destructuring excludes the finding. Unused state values remain owned by unused-variable checks. Files containing direct eval are excluded because it can observe or change lexical bindings. Indirect eval and properties named eval do not establish lexical access. Opaque initialization, property aliases, optional/dynamic paths, rest/default bindings, non-JSX hooks, generated source and tests are outside scope. This does not ban interactive useState or replace the upstream set-state-in-effect rule."
2155
2179
  ],
2156
2180
  references: [
@@ -2439,18 +2463,8 @@ var FRONTEND_IMPORT = /^(?:react(?:-dom)?|next|preact|vue|svelte|@angular\/core|
2439
2463
  function isFrontendSource(context) {
2440
2464
  return /\.[jt]sx$/u.test(context.filename) || frontendModule(context.sourceCode.ast) || frontendPackage(context.filename);
2441
2465
  }
2442
- var FRONTEND_PACKAGE_BY_DIRECTORY = /* @__PURE__ */ new Map();
2443
2466
  function frontendPackage(filename) {
2444
- const directory = dirname(resolve(filename));
2445
- let cached = FRONTEND_PACKAGE_BY_DIRECTORY.get(directory);
2446
- if (cached === void 0) {
2447
- cached = nearestManifestDeclaresFrontend(directory);
2448
- FRONTEND_PACKAGE_BY_DIRECTORY.set(directory, cached);
2449
- }
2450
- return cached;
2451
- }
2452
- function nearestManifestDeclaresFrontend(start) {
2453
- let directory = start;
2467
+ let directory = dirname(resolve(filename));
2454
2468
  for (; ; ) {
2455
2469
  const manifestPath = join(directory, "package.json");
2456
2470
  if (existsSync(manifestPath)) {
@@ -2796,6 +2810,23 @@ function ownsTestComments(context) {
2796
2810
  const framework = context.sourceCode.ast.body.some((statement) => statement.type === AST_NODE_TYPES20.ImportDeclaration && statement.importKind !== "type" && (statement.specifiers.length === 0 || statement.specifiers.some((specifier) => specifier.type !== AST_NODE_TYPES20.ImportSpecifier || specifier.importKind !== "type")) && ["vitest", "@jest/globals", "@playwright/test", "bun:test", "node:test"].includes(statement.source.value));
2797
2811
  return unambiguous || framework;
2798
2812
  }
2813
+ function legalNoticeComments(comments) {
2814
+ const legal = /* @__PURE__ */ new Set();
2815
+ for (const [index, comment] of comments.entries()) {
2816
+ if (legal.has(comment) || comment.type !== "Line" || !LEGAL.test(comment.value)) continue;
2817
+ legal.add(comment);
2818
+ for (const direction of [-1, 1]) {
2819
+ let previous = comment;
2820
+ for (let next = index + direction; next >= 0 && next < comments.length; next += direction) {
2821
+ const neighbor = comments[next];
2822
+ if (neighbor?.type !== "Line" || Math.abs(neighbor.loc.start.line - previous.loc.start.line) !== 1) break;
2823
+ legal.add(neighbor);
2824
+ previous = neighbor;
2825
+ }
2826
+ }
2827
+ }
2828
+ return legal;
2829
+ }
2799
2830
  var no_test_comments_default = createRule({
2800
2831
  name: "no-test-comments",
2801
2832
  documentation: NO_TEST_COMMENTS_DOCUMENTATION,
@@ -2814,23 +2845,10 @@ var no_test_comments_default = createRule({
2814
2845
  return {
2815
2846
  Program() {
2816
2847
  const comments = context.sourceCode.getAllComments();
2817
- const legalLines = /* @__PURE__ */ new Set();
2818
- for (const [index, comment] of comments.entries()) {
2819
- if (comment.type !== "Line" || !LEGAL.test(comment.value)) continue;
2820
- legalLines.add(index);
2821
- for (const direction of [-1, 1]) {
2822
- let previous = comment;
2823
- for (let next = index + direction; next >= 0 && next < comments.length; next += direction) {
2824
- const neighbor = comments[next];
2825
- if (neighbor?.type !== "Line" || Math.abs(neighbor.loc.start.line - previous.loc.start.line) !== 1) break;
2826
- legalLines.add(next);
2827
- previous = neighbor;
2828
- }
2829
- }
2830
- }
2831
- for (const [index, comment] of comments.entries()) {
2848
+ const legal = frontend ? void 0 : legalNoticeComments(comments);
2849
+ for (const comment of comments) {
2832
2850
  const body3 = comment.value.trim().replace(/^\*\s*/u, "");
2833
- if (!frontend && (DIRECTIVE.test(body3) || BEHAVIORAL_DIRECTIVE.test(comment.value) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value) || hasDetectSecretsPragma(comment, context.sourceCode.text))) continue;
2851
+ if (!frontend && (DIRECTIVE.test(body3) || BEHAVIORAL_DIRECTIVE.test(comment.value) || LEGAL.test(comment.value) || legal?.has(comment) || TYPE_CONTRACT.test(comment.value) || hasDetectSecretsPragma(comment, context.sourceCode.text))) continue;
2834
2852
  context.report({
2835
2853
  node: comment,
2836
2854
  messageId: "testComment",
@@ -6248,7 +6266,7 @@ function isSearchParamReceiver(call, services) {
6248
6266
  // package.json
6249
6267
  var package_default = {
6250
6268
  name: "@sarj/eslint-plugin",
6251
- version: "18.2.0",
6269
+ version: "18.2.1",
6252
6270
  packageManager: "npm@12.2.1",
6253
6271
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6254
6272
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "18.2.0",
3
+ "version": "18.2.1",
4
4
  "packageManager": "npm@12.2.1",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",