@sarj/eslint-plugin 11.1.0 → 11.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -869,6 +869,7 @@ var BANNER_RUN_RE = /={4,}|-{4,}|#{4,}|\*{4,}|~{4,}|[\u2500-\u257f]{4,}/;
869
869
  var REGION_MARKER_RE = /^#?(?:end)?region\b(.*)$/i;
870
870
  var REGION_TITLE_RE = /^[\s:\-\u2013\u2014]*\w[\w \-/&+]*$/;
871
871
  var REGION_TITLE_MAX_WORDS = 5;
872
+ var REGION_PROSE_VERB_RE = /^(?:is|are|was|were|comes?|defaults?|derives?|inherits?|depends?|uses?|maps?|resolves?)\b/i;
872
873
  function stripCommentMarker(line) {
873
874
  return line.replace(/^\s*\/{1,2}/, "").replace(/^\s*\*+/, "").trim();
874
875
  }
@@ -930,7 +931,9 @@ var CALL_RE = /^[A-Za-z_$][\w.$]*\([^)]*\)\s*;?\s*$/;
930
931
  var ASSERTION_CODE_RE = /^(?:await\s+)?(?:expect(?:TypeOf)?|assert(?:\.\w+)?)\s*\(/;
931
932
  var HTTP_CONTRACT_RE = /\b(?:GET|HEAD|OPTIONS|PATCH|POST|PUT|DELETE)\s+(?:https?:\/\/|\/|\{[A-Za-z_$])/;
932
933
  var PROSE_ASSIGNMENT_RE = /^[A-Za-z_$][\w.$[\]]*\s*=\s*[A-Za-z][A-Za-z '-]{2,}\s+\((?:how|what|when|where|which|who|why)\b[^)]*\)[.!]?$/i;
933
- var PSEUDOCODE_RE = /%\w+%|\[opt\]|(?:^|\s)<[A-Za-z]\w*>|…|\.\.\./;
934
+ var PSEUDOCODE_RE = /%\w+%|\[opt\]|(?:^|\s)<[A-Za-z]\w*>|\b[A-Za-z_$][\w$]*\s+x\d+\b|…|\.\.\./;
935
+ var CALL_LABEL_RE = /^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\([^;{}]*\)$/;
936
+ var MULTIPLIER_PSEUDOCODE_RE = /\b[A-Za-z_$][\w$]*\s+x\d+\b/;
934
937
  function isBanner(text) {
935
938
  const t = text.trim();
936
939
  if (!t) return false;
@@ -942,6 +945,7 @@ function isRegionMarker(text) {
942
945
  if (match === null) return false;
943
946
  const title = (match[1] ?? "").trim();
944
947
  if (title.length === 0) return true;
948
+ if (REGION_PROSE_VERB_RE.test(title)) return false;
945
949
  if (!REGION_TITLE_RE.test(title)) return false;
946
950
  return title.split(/\s+/).length <= REGION_TITLE_MAX_WORDS;
947
951
  }
@@ -957,6 +961,15 @@ function looksLikeCode(text, allowCall = true) {
957
961
  function hasPseudocode(text) {
958
962
  return PSEUDOCODE_RE.test(text);
959
963
  }
964
+ function testCallMatrixStems(comments) {
965
+ const stems = /* @__PURE__ */ new Set();
966
+ for (const comment of comments) {
967
+ const body2 = stripCommentMarker(comment.value);
968
+ const stem3 = CALL_LABEL_RE.exec(body2)?.[1];
969
+ if (stem3 !== void 0 && MULTIPLIER_PSEUDOCODE_RE.test(body2)) stems.add(stem3);
970
+ }
971
+ return stems;
972
+ }
960
973
  function isEnumeratedProseItem(text) {
961
974
  const t = text.trim();
962
975
  return ENUMERATED_ITEM_RE.test(t) && t.split(/\s+/).length >= ENUMERATED_ITEM_MIN_WORDS;
@@ -1245,6 +1258,7 @@ var no_comment_cruft_default = createRule({
1245
1258
  return {
1246
1259
  Program() {
1247
1260
  const comments = sourceCode.getAllComments();
1261
+ const callMatrixStems = isTestFile(context.filename) ? testCallMatrixStems(comments) : /* @__PURE__ */ new Set();
1248
1262
  const walls = findCommentWalls(comments);
1249
1263
  const wallByLeader = new Map(walls.map((wall) => [wall.leader, wall]));
1250
1264
  const wallMembers = new Set(walls.flatMap((wall) => [...wall.members]));
@@ -1282,6 +1296,8 @@ var no_comment_cruft_default = createRule({
1282
1296
  continue;
1283
1297
  }
1284
1298
  const firstText = texts[0];
1299
+ const firstTextStem = firstText === void 0 ? void 0 : CALL_LABEL_RE.exec(firstText)?.[1];
1300
+ if (firstTextStem !== void 0 && texts.length === 1 && callMatrixStems.has(firstTextStem)) continue;
1285
1301
  if (firstText !== void 0 && /^(?:todo|fixme)\b/i.test(firstText)) {
1286
1302
  if (!runCitesAReference(comments, i)) {
1287
1303
  context.report({
@@ -2211,6 +2227,20 @@ var PURE_METHODS = /* @__PURE__ */ new Set([
2211
2227
  "startsWith",
2212
2228
  "endsWith"
2213
2229
  ]);
2230
+ var SYNC_CALLBACK_METHODS = /* @__PURE__ */ new Set([
2231
+ "every",
2232
+ "filter",
2233
+ "findIndex",
2234
+ "findLast",
2235
+ "findLastIndex",
2236
+ "flatMap",
2237
+ "forEach",
2238
+ "map",
2239
+ "reduce",
2240
+ "reduceRight",
2241
+ "some",
2242
+ "sort"
2243
+ ]);
2214
2244
  var PURE_NAMESPACES = /* @__PURE__ */ new Set([
2215
2245
  "Object",
2216
2246
  "Array",
@@ -2260,7 +2290,17 @@ function isPureCall(node) {
2260
2290
  if (callee.object.type === import_utils13.AST_NODE_TYPES.Identifier && PURE_NAMESPACES.has(callee.object.name)) {
2261
2291
  return !IMPURE_NAMESPACE_METHODS.has(`${callee.object.name}.${property.name}`);
2262
2292
  }
2263
- return PURE_METHODS.has(property.name);
2293
+ return PURE_METHODS.has(property.name) && (!SYNC_CALLBACK_METHODS.has(property.name) || !synchronousCallbackCanThrow(node));
2294
+ }
2295
+ function synchronousCallbackCanThrow(node) {
2296
+ return node.arguments.some((argument) => {
2297
+ if (argument.type !== import_utils13.AST_NODE_TYPES.ArrowFunctionExpression && argument.type !== import_utils13.AST_NODE_TYPES.FunctionExpression) return false;
2298
+ if (argument.async) return false;
2299
+ return subtreeMatches2(
2300
+ argument.body,
2301
+ (current) => current.type === import_utils13.AST_NODE_TYPES.ThrowStatement || current.type === import_utils13.AST_NODE_TYPES.CallExpression && current.callee.type === import_utils13.AST_NODE_TYPES.MemberExpression && !current.callee.computed && current.callee.object.type === import_utils13.AST_NODE_TYPES.Identifier && current.callee.object.name === "JSON" && current.callee.property.type === import_utils13.AST_NODE_TYPES.Identifier && current.callee.property.name === "parse"
2302
+ );
2303
+ });
2264
2304
  }
2265
2305
  function isPureNew(node) {
2266
2306
  return node.callee.type === import_utils13.AST_NODE_TYPES.Identifier && PURE_CONSTRUCTORS.has(node.callee.name);
@@ -3522,6 +3562,8 @@ function typedFunction(node) {
3522
3562
 
3523
3563
  // src/rules/no-long-comment.ts
3524
3564
  var EXCESSIVE_SENTENCE_COUNT = 8;
3565
+ var EXCESSIVE_WORD_COUNT = 120;
3566
+ var PROSE_WORD_RE = /[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu;
3525
3567
  var VERSIONED_DEPENDENCY_TREE_RE = /(?:^|\/)lib\/[^/]*-?v?\d+\.\d+(?:\.\d+)?[^/]*\//iu;
3526
3568
  function documentsTypeOrMember(sourceCode, comment) {
3527
3569
  const token = sourceCode.getTokenAfter(comment, { includeComments: false });
@@ -3533,6 +3575,9 @@ function documentsTypeOrMember(sourceCode, comment) {
3533
3575
  }
3534
3576
  return false;
3535
3577
  }
3578
+ function wordUnits(text) {
3579
+ return text.match(PROSE_WORD_RE)?.length ?? 0;
3580
+ }
3536
3581
  var no_long_comment_default = createRule({
3537
3582
  name: "no-long-comment",
3538
3583
  meta: {
@@ -3550,7 +3595,7 @@ var no_long_comment_default = createRule({
3550
3595
  return {
3551
3596
  Program() {
3552
3597
  for (const group of proseGroups(context.filename, context.sourceCode)) {
3553
- if (group.comment.type !== "Block" || !group.comment.value.startsWith("*") || group.hasTypedTags || hasDocumentationStructure(group.text) || hasTechnicalAnchor(group.text) || documentsTypedFunction(context.sourceCode, group.comment) || documentsTypeOrMember(context.sourceCode, group.comment) || sentenceUnits(group.text) < EXCESSIVE_SENTENCE_COUNT) continue;
3598
+ if (group.comment.type !== "Block" || !group.comment.value.startsWith("*") || group.hasTypedTags || hasDocumentationStructure(group.text) || hasTechnicalAnchor(group.text) || documentsTypedFunction(context.sourceCode, group.comment) || documentsTypeOrMember(context.sourceCode, group.comment) || sentenceUnits(group.text) < EXCESSIVE_SENTENCE_COUNT && wordUnits(group.text) < EXCESSIVE_WORD_COUNT) continue;
3554
3599
  context.report({ node: group.comment, messageId: "tooLong" });
3555
3600
  }
3556
3601
  }
@@ -8554,6 +8599,11 @@ var prefer_module_level_schema_default = createRule({
8554
8599
  type: "boolean",
8555
8600
  description: "Skip test files, where a fixture schema belongs next to its assertion."
8556
8601
  },
8602
+ memoCallees: {
8603
+ type: "array",
8604
+ items: { type: "string" },
8605
+ description: "Wrappers that construct their callback at most once. Defaults to lazy, memo, once, and useMemo."
8606
+ },
8557
8607
  minProperties: {
8558
8608
  type: "number",
8559
8609
  minimum: 0,
@@ -8571,6 +8621,7 @@ var prefer_module_level_schema_default = createRule({
8571
8621
  create(context, [options]) {
8572
8622
  const factories = new Set(options?.factories ?? DEFAULT_FACTORIES);
8573
8623
  const ignoreTestFiles = options?.ignoreTestFiles ?? true;
8624
+ const memoCallees = new Set(options?.memoCallees ?? MEMO_CALLEES);
8574
8625
  const minProperties = options?.minProperties ?? DEFAULT_MIN_PROPERTIES;
8575
8626
  const sourceCode = context.sourceCode;
8576
8627
  const filename = context.filename;
@@ -8590,7 +8641,7 @@ var prefer_module_level_schema_default = createRule({
8590
8641
  if (current !== node && isZodCall(current) && current.callee.type === import_utils56.AST_NODE_TYPES.MemberExpression && current.callee.property.type === import_utils56.AST_NODE_TYPES.Identifier && factories.has(current.callee.property.name)) {
8591
8642
  return true;
8592
8643
  }
8593
- if (current.type === import_utils56.AST_NODE_TYPES.CallExpression && (current.callee.type === import_utils56.AST_NODE_TYPES.Identifier && MEMO_CALLEES.has(current.callee.name) || current.callee.type === import_utils56.AST_NODE_TYPES.MemberExpression && !current.callee.computed && current.callee.property.type === import_utils56.AST_NODE_TYPES.Identifier && MEMO_CALLEES.has(current.callee.property.name))) {
8644
+ if (current.type === import_utils56.AST_NODE_TYPES.CallExpression && (current.callee.type === import_utils56.AST_NODE_TYPES.Identifier && memoCallees.has(current.callee.name) || current.callee.type === import_utils56.AST_NODE_TYPES.MemberExpression && !current.callee.computed && current.callee.property.type === import_utils56.AST_NODE_TYPES.Identifier && memoCallees.has(current.callee.property.name))) {
8594
8645
  return true;
8595
8646
  }
8596
8647
  current = current.parent ?? void 0;
@@ -8640,6 +8691,9 @@ var prefer_module_level_schema_default = createRule({
8640
8691
  if (definition.type === "ImportBinding") {
8641
8692
  continue;
8642
8693
  }
8694
+ if (definition.node.type === import_utils56.AST_NODE_TYPES.VariableDeclarator && definition.node.parent.type === import_utils56.AST_NODE_TYPES.VariableDeclaration && definition.node.parent.kind !== "const") {
8695
+ return false;
8696
+ }
8643
8697
  const [defStart, defEnd] = definition.node.range;
8644
8698
  if (defStart >= schemaStart && defEnd <= schemaEnd) {
8645
8699
  continue;
@@ -8874,7 +8928,7 @@ function sameAccess(node, access) {
8874
8928
  return node.type === import_utils58.AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === import_utils58.AST_NODE_TYPES.Identifier && node.object.name === access.object && node.property.type === import_utils58.AST_NODE_TYPES.Identifier && node.property.name === access.property;
8875
8929
  }
8876
8930
  function isNullGuard(node, access) {
8877
- if (node.type === import_utils58.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && sameAccess(node.argument, access)) return true;
8931
+ if (node.type === import_utils58.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
8878
8932
  if (node.type !== import_utils58.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(node.operator)) {
8879
8933
  return false;
8880
8934
  }
@@ -8890,7 +8944,11 @@ function isEmptyGuard(node, access) {
8890
8944
  return memberLengthOf(node.left, access) && zero(node.right) || memberLengthOf(node.right, access) && zero(node.left);
8891
8945
  }
8892
8946
  function memberLengthOf(node, access) {
8893
- return node.type === import_utils58.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils58.AST_NODE_TYPES.Identifier && node.property.name === "length" && sameAccess(node.object, access);
8947
+ const target = node.type === import_utils58.AST_NODE_TYPES.ChainExpression ? node.expression : node;
8948
+ return target.type === import_utils58.AST_NODE_TYPES.MemberExpression && !target.computed && target.property.type === import_utils58.AST_NODE_TYPES.Identifier && target.property.name === "length" && sameAccess(target.object, access);
8949
+ }
8950
+ function optionalMemberLengthOf(node, access) {
8951
+ return node.type === import_utils58.AST_NODE_TYPES.ChainExpression && node.expression.type === import_utils58.AST_NODE_TYPES.MemberExpression && node.expression.optional && memberLengthOf(node, access);
8894
8952
  }
8895
8953
  function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
8896
8954
  if (fn.body.type !== import_utils58.AST_NODE_TYPES.BlockStatement) return false;
@@ -8898,6 +8956,7 @@ function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
8898
8956
  if (first?.type !== import_utils58.AST_NODE_TYPES.IfStatement) return false;
8899
8957
  const terminating = first.consequent.type === import_utils58.AST_NODE_TYPES.ReturnStatement || first.consequent.type === import_utils58.AST_NODE_TYPES.ThrowStatement || first.consequent.type === import_utils58.AST_NODE_TYPES.BlockStatement && first.consequent.body.length === 1 && (first.consequent.body[0]?.type === import_utils58.AST_NODE_TYPES.ReturnStatement || first.consequent.body[0]?.type === import_utils58.AST_NODE_TYPES.ThrowStatement);
8900
8958
  if (!terminating) return false;
8959
+ if (contains(first.consequent, visitorKeys, (node) => sameAccess(node, access))) return false;
8901
8960
  return contains(first.test, visitorKeys, (node) => isNullGuard(node, access)) && contains(first.test, visitorKeys, (node) => isEmptyGuard(node, access));
8902
8961
  }
8903
8962
  function contains(node, visitorKeys, predicate) {
@@ -12641,7 +12700,7 @@ var rules = {
12641
12700
  };
12642
12701
  var meta = {
12643
12702
  name: "@sarj/eslint-plugin",
12644
- version: "11.1.0"
12703
+ version: "11.2.0"
12645
12704
  };
12646
12705
  var applicationOnlyRules = [
12647
12706
  "no-restricted-library-load",
package/dist/index.d.cts CHANGED
@@ -246,6 +246,7 @@ declare const rules: {
246
246
  readonly "prefer-module-level-schema": _typescript_eslint_utils_ts_eslint.RuleModule<"hoistSchema", readonly [{
247
247
  factories?: readonly string[];
248
248
  ignoreTestFiles?: boolean;
249
+ memoCallees?: readonly string[];
249
250
  minProperties?: number;
250
251
  }?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
251
252
  name: string;
@@ -453,7 +454,7 @@ type FlatPreset = {
453
454
  declare const plugin: {
454
455
  readonly meta: {
455
456
  readonly name: "@sarj/eslint-plugin";
456
- readonly version: "11.1.0";
457
+ readonly version: "11.2.0";
457
458
  };
458
459
  readonly rules: {
459
460
  readonly "duplicate-test-body": _typescript_eslint_utils_ts_eslint.RuleModule<"duplicateTestBody", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -614,6 +615,7 @@ declare const plugin: {
614
615
  readonly "prefer-module-level-schema": _typescript_eslint_utils_ts_eslint.RuleModule<"hoistSchema", readonly [{
615
616
  factories?: readonly string[];
616
617
  ignoreTestFiles?: boolean;
618
+ memoCallees?: readonly string[];
617
619
  minProperties?: number;
618
620
  }?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
619
621
  name: string;
package/dist/index.d.ts CHANGED
@@ -246,6 +246,7 @@ declare const rules: {
246
246
  readonly "prefer-module-level-schema": _typescript_eslint_utils_ts_eslint.RuleModule<"hoistSchema", readonly [{
247
247
  factories?: readonly string[];
248
248
  ignoreTestFiles?: boolean;
249
+ memoCallees?: readonly string[];
249
250
  minProperties?: number;
250
251
  }?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
251
252
  name: string;
@@ -453,7 +454,7 @@ type FlatPreset = {
453
454
  declare const plugin: {
454
455
  readonly meta: {
455
456
  readonly name: "@sarj/eslint-plugin";
456
- readonly version: "11.1.0";
457
+ readonly version: "11.2.0";
457
458
  };
458
459
  readonly rules: {
459
460
  readonly "duplicate-test-body": _typescript_eslint_utils_ts_eslint.RuleModule<"duplicateTestBody", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
@@ -614,6 +615,7 @@ declare const plugin: {
614
615
  readonly "prefer-module-level-schema": _typescript_eslint_utils_ts_eslint.RuleModule<"hoistSchema", readonly [{
615
616
  factories?: readonly string[];
616
617
  ignoreTestFiles?: boolean;
618
+ memoCallees?: readonly string[];
617
619
  minProperties?: number;
618
620
  }?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
619
621
  name: string;
package/dist/index.js CHANGED
@@ -827,6 +827,7 @@ var BANNER_RUN_RE = /={4,}|-{4,}|#{4,}|\*{4,}|~{4,}|[\u2500-\u257f]{4,}/;
827
827
  var REGION_MARKER_RE = /^#?(?:end)?region\b(.*)$/i;
828
828
  var REGION_TITLE_RE = /^[\s:\-\u2013\u2014]*\w[\w \-/&+]*$/;
829
829
  var REGION_TITLE_MAX_WORDS = 5;
830
+ var REGION_PROSE_VERB_RE = /^(?:is|are|was|were|comes?|defaults?|derives?|inherits?|depends?|uses?|maps?|resolves?)\b/i;
830
831
  function stripCommentMarker(line) {
831
832
  return line.replace(/^\s*\/{1,2}/, "").replace(/^\s*\*+/, "").trim();
832
833
  }
@@ -888,7 +889,9 @@ var CALL_RE = /^[A-Za-z_$][\w.$]*\([^)]*\)\s*;?\s*$/;
888
889
  var ASSERTION_CODE_RE = /^(?:await\s+)?(?:expect(?:TypeOf)?|assert(?:\.\w+)?)\s*\(/;
889
890
  var HTTP_CONTRACT_RE = /\b(?:GET|HEAD|OPTIONS|PATCH|POST|PUT|DELETE)\s+(?:https?:\/\/|\/|\{[A-Za-z_$])/;
890
891
  var PROSE_ASSIGNMENT_RE = /^[A-Za-z_$][\w.$[\]]*\s*=\s*[A-Za-z][A-Za-z '-]{2,}\s+\((?:how|what|when|where|which|who|why)\b[^)]*\)[.!]?$/i;
891
- var PSEUDOCODE_RE = /%\w+%|\[opt\]|(?:^|\s)<[A-Za-z]\w*>|…|\.\.\./;
892
+ var PSEUDOCODE_RE = /%\w+%|\[opt\]|(?:^|\s)<[A-Za-z]\w*>|\b[A-Za-z_$][\w$]*\s+x\d+\b|…|\.\.\./;
893
+ var CALL_LABEL_RE = /^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\([^;{}]*\)$/;
894
+ var MULTIPLIER_PSEUDOCODE_RE = /\b[A-Za-z_$][\w$]*\s+x\d+\b/;
892
895
  function isBanner(text) {
893
896
  const t = text.trim();
894
897
  if (!t) return false;
@@ -900,6 +903,7 @@ function isRegionMarker(text) {
900
903
  if (match === null) return false;
901
904
  const title = (match[1] ?? "").trim();
902
905
  if (title.length === 0) return true;
906
+ if (REGION_PROSE_VERB_RE.test(title)) return false;
903
907
  if (!REGION_TITLE_RE.test(title)) return false;
904
908
  return title.split(/\s+/).length <= REGION_TITLE_MAX_WORDS;
905
909
  }
@@ -915,6 +919,15 @@ function looksLikeCode(text, allowCall = true) {
915
919
  function hasPseudocode(text) {
916
920
  return PSEUDOCODE_RE.test(text);
917
921
  }
922
+ function testCallMatrixStems(comments) {
923
+ const stems = /* @__PURE__ */ new Set();
924
+ for (const comment of comments) {
925
+ const body2 = stripCommentMarker(comment.value);
926
+ const stem3 = CALL_LABEL_RE.exec(body2)?.[1];
927
+ if (stem3 !== void 0 && MULTIPLIER_PSEUDOCODE_RE.test(body2)) stems.add(stem3);
928
+ }
929
+ return stems;
930
+ }
918
931
  function isEnumeratedProseItem(text) {
919
932
  const t = text.trim();
920
933
  return ENUMERATED_ITEM_RE.test(t) && t.split(/\s+/).length >= ENUMERATED_ITEM_MIN_WORDS;
@@ -1203,6 +1216,7 @@ var no_comment_cruft_default = createRule({
1203
1216
  return {
1204
1217
  Program() {
1205
1218
  const comments = sourceCode.getAllComments();
1219
+ const callMatrixStems = isTestFile(context.filename) ? testCallMatrixStems(comments) : /* @__PURE__ */ new Set();
1206
1220
  const walls = findCommentWalls(comments);
1207
1221
  const wallByLeader = new Map(walls.map((wall) => [wall.leader, wall]));
1208
1222
  const wallMembers = new Set(walls.flatMap((wall) => [...wall.members]));
@@ -1240,6 +1254,8 @@ var no_comment_cruft_default = createRule({
1240
1254
  continue;
1241
1255
  }
1242
1256
  const firstText = texts[0];
1257
+ const firstTextStem = firstText === void 0 ? void 0 : CALL_LABEL_RE.exec(firstText)?.[1];
1258
+ if (firstTextStem !== void 0 && texts.length === 1 && callMatrixStems.has(firstTextStem)) continue;
1243
1259
  if (firstText !== void 0 && /^(?:todo|fixme)\b/i.test(firstText)) {
1244
1260
  if (!runCitesAReference(comments, i)) {
1245
1261
  context.report({
@@ -2169,6 +2185,20 @@ var PURE_METHODS = /* @__PURE__ */ new Set([
2169
2185
  "startsWith",
2170
2186
  "endsWith"
2171
2187
  ]);
2188
+ var SYNC_CALLBACK_METHODS = /* @__PURE__ */ new Set([
2189
+ "every",
2190
+ "filter",
2191
+ "findIndex",
2192
+ "findLast",
2193
+ "findLastIndex",
2194
+ "flatMap",
2195
+ "forEach",
2196
+ "map",
2197
+ "reduce",
2198
+ "reduceRight",
2199
+ "some",
2200
+ "sort"
2201
+ ]);
2172
2202
  var PURE_NAMESPACES = /* @__PURE__ */ new Set([
2173
2203
  "Object",
2174
2204
  "Array",
@@ -2218,7 +2248,17 @@ function isPureCall(node) {
2218
2248
  if (callee.object.type === AST_NODE_TYPES10.Identifier && PURE_NAMESPACES.has(callee.object.name)) {
2219
2249
  return !IMPURE_NAMESPACE_METHODS.has(`${callee.object.name}.${property.name}`);
2220
2250
  }
2221
- return PURE_METHODS.has(property.name);
2251
+ return PURE_METHODS.has(property.name) && (!SYNC_CALLBACK_METHODS.has(property.name) || !synchronousCallbackCanThrow(node));
2252
+ }
2253
+ function synchronousCallbackCanThrow(node) {
2254
+ return node.arguments.some((argument) => {
2255
+ if (argument.type !== AST_NODE_TYPES10.ArrowFunctionExpression && argument.type !== AST_NODE_TYPES10.FunctionExpression) return false;
2256
+ if (argument.async) return false;
2257
+ return subtreeMatches2(
2258
+ argument.body,
2259
+ (current) => current.type === AST_NODE_TYPES10.ThrowStatement || current.type === AST_NODE_TYPES10.CallExpression && current.callee.type === AST_NODE_TYPES10.MemberExpression && !current.callee.computed && current.callee.object.type === AST_NODE_TYPES10.Identifier && current.callee.object.name === "JSON" && current.callee.property.type === AST_NODE_TYPES10.Identifier && current.callee.property.name === "parse"
2260
+ );
2261
+ });
2222
2262
  }
2223
2263
  function isPureNew(node) {
2224
2264
  return node.callee.type === AST_NODE_TYPES10.Identifier && PURE_CONSTRUCTORS.has(node.callee.name);
@@ -3480,6 +3520,8 @@ function typedFunction(node) {
3480
3520
 
3481
3521
  // src/rules/no-long-comment.ts
3482
3522
  var EXCESSIVE_SENTENCE_COUNT = 8;
3523
+ var EXCESSIVE_WORD_COUNT = 120;
3524
+ var PROSE_WORD_RE = /[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu;
3483
3525
  var VERSIONED_DEPENDENCY_TREE_RE = /(?:^|\/)lib\/[^/]*-?v?\d+\.\d+(?:\.\d+)?[^/]*\//iu;
3484
3526
  function documentsTypeOrMember(sourceCode, comment) {
3485
3527
  const token = sourceCode.getTokenAfter(comment, { includeComments: false });
@@ -3491,6 +3533,9 @@ function documentsTypeOrMember(sourceCode, comment) {
3491
3533
  }
3492
3534
  return false;
3493
3535
  }
3536
+ function wordUnits(text) {
3537
+ return text.match(PROSE_WORD_RE)?.length ?? 0;
3538
+ }
3494
3539
  var no_long_comment_default = createRule({
3495
3540
  name: "no-long-comment",
3496
3541
  meta: {
@@ -3508,7 +3553,7 @@ var no_long_comment_default = createRule({
3508
3553
  return {
3509
3554
  Program() {
3510
3555
  for (const group of proseGroups(context.filename, context.sourceCode)) {
3511
- if (group.comment.type !== "Block" || !group.comment.value.startsWith("*") || group.hasTypedTags || hasDocumentationStructure(group.text) || hasTechnicalAnchor(group.text) || documentsTypedFunction(context.sourceCode, group.comment) || documentsTypeOrMember(context.sourceCode, group.comment) || sentenceUnits(group.text) < EXCESSIVE_SENTENCE_COUNT) continue;
3556
+ if (group.comment.type !== "Block" || !group.comment.value.startsWith("*") || group.hasTypedTags || hasDocumentationStructure(group.text) || hasTechnicalAnchor(group.text) || documentsTypedFunction(context.sourceCode, group.comment) || documentsTypeOrMember(context.sourceCode, group.comment) || sentenceUnits(group.text) < EXCESSIVE_SENTENCE_COUNT && wordUnits(group.text) < EXCESSIVE_WORD_COUNT) continue;
3512
3557
  context.report({ node: group.comment, messageId: "tooLong" });
3513
3558
  }
3514
3559
  }
@@ -8515,6 +8560,11 @@ var prefer_module_level_schema_default = createRule({
8515
8560
  type: "boolean",
8516
8561
  description: "Skip test files, where a fixture schema belongs next to its assertion."
8517
8562
  },
8563
+ memoCallees: {
8564
+ type: "array",
8565
+ items: { type: "string" },
8566
+ description: "Wrappers that construct their callback at most once. Defaults to lazy, memo, once, and useMemo."
8567
+ },
8518
8568
  minProperties: {
8519
8569
  type: "number",
8520
8570
  minimum: 0,
@@ -8532,6 +8582,7 @@ var prefer_module_level_schema_default = createRule({
8532
8582
  create(context, [options]) {
8533
8583
  const factories = new Set(options?.factories ?? DEFAULT_FACTORIES);
8534
8584
  const ignoreTestFiles = options?.ignoreTestFiles ?? true;
8585
+ const memoCallees = new Set(options?.memoCallees ?? MEMO_CALLEES);
8535
8586
  const minProperties = options?.minProperties ?? DEFAULT_MIN_PROPERTIES;
8536
8587
  const sourceCode = context.sourceCode;
8537
8588
  const filename = context.filename;
@@ -8551,7 +8602,7 @@ var prefer_module_level_schema_default = createRule({
8551
8602
  if (current !== node && isZodCall(current) && current.callee.type === AST_NODE_TYPES42.MemberExpression && current.callee.property.type === AST_NODE_TYPES42.Identifier && factories.has(current.callee.property.name)) {
8552
8603
  return true;
8553
8604
  }
8554
- if (current.type === AST_NODE_TYPES42.CallExpression && (current.callee.type === AST_NODE_TYPES42.Identifier && MEMO_CALLEES.has(current.callee.name) || current.callee.type === AST_NODE_TYPES42.MemberExpression && !current.callee.computed && current.callee.property.type === AST_NODE_TYPES42.Identifier && MEMO_CALLEES.has(current.callee.property.name))) {
8605
+ if (current.type === AST_NODE_TYPES42.CallExpression && (current.callee.type === AST_NODE_TYPES42.Identifier && memoCallees.has(current.callee.name) || current.callee.type === AST_NODE_TYPES42.MemberExpression && !current.callee.computed && current.callee.property.type === AST_NODE_TYPES42.Identifier && memoCallees.has(current.callee.property.name))) {
8555
8606
  return true;
8556
8607
  }
8557
8608
  current = current.parent ?? void 0;
@@ -8601,6 +8652,9 @@ var prefer_module_level_schema_default = createRule({
8601
8652
  if (definition.type === "ImportBinding") {
8602
8653
  continue;
8603
8654
  }
8655
+ if (definition.node.type === AST_NODE_TYPES42.VariableDeclarator && definition.node.parent.type === AST_NODE_TYPES42.VariableDeclaration && definition.node.parent.kind !== "const") {
8656
+ return false;
8657
+ }
8604
8658
  const [defStart, defEnd] = definition.node.range;
8605
8659
  if (defStart >= schemaStart && defEnd <= schemaEnd) {
8606
8660
  continue;
@@ -8835,7 +8889,7 @@ function sameAccess(node, access) {
8835
8889
  return node.type === AST_NODE_TYPES44.MemberExpression && !node.computed && node.object.type === AST_NODE_TYPES44.Identifier && node.object.name === access.object && node.property.type === AST_NODE_TYPES44.Identifier && node.property.name === access.property;
8836
8890
  }
8837
8891
  function isNullGuard(node, access) {
8838
- if (node.type === AST_NODE_TYPES44.UnaryExpression && node.operator === "!" && sameAccess(node.argument, access)) return true;
8892
+ if (node.type === AST_NODE_TYPES44.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
8839
8893
  if (node.type !== AST_NODE_TYPES44.BinaryExpression || !["==", "==="].includes(node.operator)) {
8840
8894
  return false;
8841
8895
  }
@@ -8851,7 +8905,11 @@ function isEmptyGuard(node, access) {
8851
8905
  return memberLengthOf(node.left, access) && zero(node.right) || memberLengthOf(node.right, access) && zero(node.left);
8852
8906
  }
8853
8907
  function memberLengthOf(node, access) {
8854
- return node.type === AST_NODE_TYPES44.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES44.Identifier && node.property.name === "length" && sameAccess(node.object, access);
8908
+ const target = node.type === AST_NODE_TYPES44.ChainExpression ? node.expression : node;
8909
+ return target.type === AST_NODE_TYPES44.MemberExpression && !target.computed && target.property.type === AST_NODE_TYPES44.Identifier && target.property.name === "length" && sameAccess(target.object, access);
8910
+ }
8911
+ function optionalMemberLengthOf(node, access) {
8912
+ return node.type === AST_NODE_TYPES44.ChainExpression && node.expression.type === AST_NODE_TYPES44.MemberExpression && node.expression.optional && memberLengthOf(node, access);
8855
8913
  }
8856
8914
  function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
8857
8915
  if (fn.body.type !== AST_NODE_TYPES44.BlockStatement) return false;
@@ -8859,6 +8917,7 @@ function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
8859
8917
  if (first?.type !== AST_NODE_TYPES44.IfStatement) return false;
8860
8918
  const terminating = first.consequent.type === AST_NODE_TYPES44.ReturnStatement || first.consequent.type === AST_NODE_TYPES44.ThrowStatement || first.consequent.type === AST_NODE_TYPES44.BlockStatement && first.consequent.body.length === 1 && (first.consequent.body[0]?.type === AST_NODE_TYPES44.ReturnStatement || first.consequent.body[0]?.type === AST_NODE_TYPES44.ThrowStatement);
8861
8919
  if (!terminating) return false;
8920
+ if (contains(first.consequent, visitorKeys, (node) => sameAccess(node, access))) return false;
8862
8921
  return contains(first.test, visitorKeys, (node) => isNullGuard(node, access)) && contains(first.test, visitorKeys, (node) => isEmptyGuard(node, access));
8863
8922
  }
8864
8923
  function contains(node, visitorKeys, predicate) {
@@ -12605,7 +12664,7 @@ var rules = {
12605
12664
  };
12606
12665
  var meta = {
12607
12666
  name: "@sarj/eslint-plugin",
12608
- version: "11.1.0"
12667
+ version: "11.2.0"
12609
12668
  };
12610
12669
  var applicationOnlyRules = [
12611
12670
  "no-restricted-library-load",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "11.1.0",
3
+ "version": "11.2.0",
4
4
  "packageManager": "npm@11.19.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",