@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 +64 -46
- package/dist/index.js +64 -46
- package/package.json +1 -1
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.
|
|
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.
|
|
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) =>
|
|
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
|
|
1883
|
-
|
|
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
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
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
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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
|
-
|
|
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
|
|
2860
|
-
for (const
|
|
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) ||
|
|
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.
|
|
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.
|
|
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.
|
|
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) =>
|
|
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
|
|
1841
|
-
|
|
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
|
|
1850
|
-
if (
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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
|
-
|
|
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
|
|
2818
|
-
for (const
|
|
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) ||
|
|
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.
|
|
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",
|