@sarj/eslint-plugin 17.1.0 → 17.1.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
@@ -164,28 +164,21 @@ function hasTypeInformation(services) {
164
164
  return services?.program != null && services.esTreeNodeToTSNodeMap != null && services.tsNodeToESTreeNodeMap != null;
165
165
  }
166
166
  function createNativeRule(config, url) {
167
- const input = { ...config };
168
- const originalMeta = Object.hasOwn(input, "meta") ? input.meta : config.meta;
167
+ const name = config.name;
168
+ const originalMeta = config.meta;
169
169
  const metadataSnapshot = { ...originalMeta };
170
170
  const declaredMetaDefaults = Object.hasOwn(metadataSnapshot, "defaultOptions") ? metadataSnapshot.defaultOptions : originalMeta.defaultOptions;
171
- const declaredLegacyDefaults = Object.hasOwn(input, "defaultOptions") ? input.defaultOptions : config.defaultOptions;
172
- const descriptor = {
173
- ...input,
174
- name: Object.hasOwn(input, "name") ? input.name : config.name,
175
- create: Object.hasOwn(input, "create") ? input.create : config.create,
176
- meta: metadataSnapshot
177
- };
178
- if (declaredLegacyDefaults !== void 0) descriptor.defaultOptions = declaredLegacyDefaults;
179
- if (declaredMetaDefaults !== void 0) descriptor.meta.defaultOptions = declaredMetaDefaults;
180
- const { defaultOptions: metaDefaults, schema, ...metadata } = descriptor.meta;
171
+ const declaredLegacyDefaults = config.defaultOptions;
172
+ if (declaredMetaDefaults !== void 0) metadataSnapshot.defaultOptions = declaredMetaDefaults;
173
+ const { defaultOptions: metaDefaults, schema, ...metadata } = metadataSnapshot;
181
174
  const metadataDefaults = metaDefaults === void 0 ? void 0 : ownTuple(metaDefaults);
182
- const legacyDefaults = metadataDefaults === void 0 && descriptor.defaultOptions !== void 0 ? ownTuple(descriptor.defaultOptions) : void 0;
175
+ const legacyDefaults = metadataDefaults === void 0 && declaredLegacyDefaults !== void 0 ? ownTuple(declaredLegacyDefaults) : void 0;
183
176
  const ownedMetadata = {
184
177
  ...metadata,
185
- docs: { ...metadata.docs, url: url(descriptor.name) },
178
+ docs: { ...metadata.docs, url: url(name) },
186
179
  schema: schemaList(schema) ? [...schema] : schema
187
180
  };
188
- if (metadataDefaults === void 0 && Object.hasOwn(descriptor.meta, "defaultOptions")) {
181
+ if (metadataDefaults === void 0 && Object.hasOwn(metadataSnapshot, "defaultOptions")) {
189
182
  Object.defineProperty(ownedMetadata, "defaultOptions", {
190
183
  value: void 0,
191
184
  configurable: true,
@@ -195,25 +188,25 @@ function createNativeRule(config, url) {
195
188
  }
196
189
  const meta2 = metadataDefaults === void 0 ? ownedMetadata : { ...ownedMetadata, defaultOptions: metadataDefaults };
197
190
  const base = {
198
- name: descriptor.name,
199
- defaultOptions: descriptor.defaultOptions,
191
+ name,
192
+ defaultOptions: declaredLegacyDefaults,
200
193
  meta: meta2
201
194
  };
202
- if (hasNoDefaultOptions(descriptor)) {
203
- const create2 = descriptor.create;
195
+ if (hasNoDefaultOptions(config, declaredMetaDefaults, declaredLegacyDefaults)) {
196
+ const create2 = config.create;
204
197
  return { ...base, create(context) {
205
198
  return create2(context, []);
206
199
  } };
207
200
  }
208
201
  const defaults = metadataDefaults ?? legacyDefaults;
209
202
  if (defaults === void 0) throw new TypeError("Declared defaults disappeared while creating the rule.");
210
- const create = descriptor.create;
203
+ const create = config.create;
211
204
  return { ...base, create(context) {
212
205
  return create(context, import_utils2.ESLintUtils.applyDefault(defaults, context.options));
213
206
  } };
214
207
  }
215
- function hasNoDefaultOptions(config) {
216
- return config.meta.defaultOptions === void 0 && config.defaultOptions === void 0;
208
+ function hasNoDefaultOptions(_config, metaDefaults, legacyDefaults) {
209
+ return metaDefaults === void 0 && legacyDefaults === void 0;
217
210
  }
218
211
  function schemaList(schema) {
219
212
  return Array.isArray(schema);
@@ -231,13 +224,12 @@ var KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
231
224
  var MAX_SUMMARY_LENGTH = 160;
232
225
  function createRule(config) {
233
226
  const { documentation } = config;
234
- const eslintConfig = config;
235
- const rule = createNativeRule(eslintConfig, examplesUrl);
227
+ const rule = createNativeRule(config, examplesUrl);
236
228
  if (documentation !== void 0) {
237
229
  Object.defineProperty(rule, "documentation", {
238
230
  configurable: false,
239
231
  enumerable: false,
240
- value: nativeSpec(eslintConfig, documentation),
232
+ value: nativeSpec(rule, documentation),
241
233
  writable: false
242
234
  });
243
235
  }
@@ -1026,19 +1018,22 @@ var NO_FRONTEND_COMMENTS_DOCUMENTATION = {
1026
1018
  defaultLevel: "warning",
1027
1019
  summary: "Express frontend behavior through code instead of prose comments.",
1028
1020
  rationale: "Frontend comments can drift from executable behavior and conceal names, types or structure that should communicate the same information.",
1029
- remediation: "Use clear names, types, helpers and assertions, then remove the comment. Move tool settings into their owning configuration where supported. Review behavioral directives and legal notices before editing; no automatic deletion changes those contracts.",
1021
+ remediation: "Use clear names, types, helpers and assertions, then remove the comment. Move tool settings into their owning configuration where supported. Whitespace-only single-line comments can be replaced with a space; review all other comments and their contracts manually.",
1030
1022
  category: "maintainability",
1031
- autofix: "none",
1023
+ autofix: "safe",
1032
1024
  limitations: [
1033
1025
  "Reports every actual line, block, JSDoc and JSX comment in maintained frontend source, including legal notices and executable directives. The existing no-test-comments rule owns maintained test files and applies the same blanket frontend ban, avoiding duplicate diagnostics between these two rules. Strings, template data, rendered JSX text and generated/vendor sources are excluded.",
1034
1026
  "Frontend scope is established by JSX-capable extensions, use client/use server directives, framework imports, or the nearest package manifest declaring a frontend framework dependency. Pure helpers in those packages are included. Set allFiles: true on both this rule and no-test-comments in a frontend ESLint override for dependency-free or unsupported frameworks; file names alone do not infer application ownership.",
1035
- "This is a blanket policy warning, including comments preserved by narrower comment-hygiene rules. Behavioral annotations, JavaScript type comments, required legal notices and tool directives require manual migration or an explicit policy exception; the rule never automatically deletes them."
1027
+ "This is a blanket policy warning, including comments preserved by narrower comment-hygiene rules. Behavioral annotations, JavaScript type comments, required legal notices and tool directives require manual migration or an explicit policy exception; the rule never automatically deletes them.",
1028
+ "Automatic fixes replace only whitespace-only line or block comment tokens containing no line terminators with one separating space. Every existing line terminator is preserved. Comments overlapping JSX regions, multiline comments, nonempty comments and comments owned by the test rule remain manual."
1036
1029
  ],
1037
1030
  examples: [
1038
1031
  { id: "frontend-prose", title: "Express view behavior in code", outcome: "match", files: [{ path: "src/view.tsx", source: "// Show current status\nexport const View = () => <p>Ready</p>;" }], focusPath: "src/view.tsx", expectedCount: 1, public: true },
1039
1032
  { id: "named-view", title: "The view expresses its behavior", outcome: "no-match", files: [{ path: "src/view.tsx", source: "export const View = () => <p>Ready</p>;" }], focusPath: "src/view.tsx", expectedCount: 0, public: true },
1040
1033
  { id: "package-helper", scenarioId: "package", title: "A plain helper belongs to its frontend package", outcome: "match", files: [{ path: "package.json", source: '{"private":true,"dependencies":{"react":"19.2.0"}}' }, { path: "src/helper.ts", source: "// Frontend helper\nexport const ready = true;" }], focusPath: "src/helper.ts", expectedCount: 1, public: true },
1041
- { id: "package-helper-code", scenarioId: "package", title: "The helper expresses its behavior in code", outcome: "no-match", files: [{ path: "package.json", source: '{"private":true,"dependencies":{"react":"19.2.0"}}' }, { path: "src/helper.ts", source: "export const ready = true;" }], focusPath: "src/helper.ts", expectedCount: 0, public: true }
1034
+ { id: "package-helper-code", scenarioId: "package", title: "The helper expresses its behavior in code", outcome: "no-match", files: [{ path: "package.json", source: '{"private":true,"dependencies":{"react":"19.2.0"}}' }, { path: "src/helper.ts", source: "export const ready = true;" }], focusPath: "src/helper.ts", expectedCount: 0, public: true },
1035
+ { id: "empty-comment-separator", scenarioId: "empty-comment", title: "Keep a separating space when removing an empty comment", outcome: "match", files: [{ path: "src/helper.tsx", source: "export function identity(value: number) { return/**/value; }" }], fixedFiles: [{ path: "src/helper.tsx", source: "export function identity(value: number) { return value; }" }], focusPath: "src/helper.tsx", expectedCount: 1, public: true },
1036
+ { id: "empty-comment-removed", scenarioId: "empty-comment", title: "Preserve the return expression without a comment", outcome: "no-match", files: [{ path: "src/helper.tsx", source: "export function identity(value: number) { return value; }" }], focusPath: "src/helper.tsx", expectedCount: 0, public: true }
1042
1037
  ]
1043
1038
  };
1044
1039
  var no_frontend_comments_default = createRule({
@@ -1046,6 +1041,7 @@ var no_frontend_comments_default = createRule({
1046
1041
  documentation: NO_FRONTEND_COMMENTS_DOCUMENTATION,
1047
1042
  meta: {
1048
1043
  type: "suggestion",
1044
+ fixable: "code",
1049
1045
  docs: { description: NO_FRONTEND_COMMENTS_DOCUMENTATION.summary },
1050
1046
  schema: [{ type: "object", properties: { allFiles: { type: "boolean" } }, additionalProperties: false }],
1051
1047
  messages: { noFrontendComments: "Frontend comments are prohibited; express the behavior in names, types or code and review any directive or legal contract before removing it." }
@@ -1053,15 +1049,76 @@ var no_frontend_comments_default = createRule({
1053
1049
  defaultOptions: [{}],
1054
1050
  create(context, [options]) {
1055
1051
  if (isGeneratedFile(context, context.sourceCode.text) || ownsTestComments(context)) return {};
1052
+ const jsxRanges = [];
1053
+ let ambiguousJSX = false;
1054
+ function captureJSX(node) {
1055
+ const range = sourceRange(node.range, context.sourceCode.text.length);
1056
+ if (range === null) {
1057
+ ambiguousJSX = true;
1058
+ return;
1059
+ }
1060
+ const previous = jsxRanges.at(-1);
1061
+ if (previous === void 0 || range.start >= previous.end) {
1062
+ jsxRanges.push(range);
1063
+ return;
1064
+ }
1065
+ if (range.start < previous.start || range.end > previous.end) ambiguousJSX = true;
1066
+ }
1056
1067
  return {
1057
- Program() {
1068
+ JSXElement: captureJSX,
1069
+ JSXFragment: captureJSX,
1070
+ "Program:exit"() {
1058
1071
  const comments = context.sourceCode.getAllComments();
1059
1072
  if (comments.length === 0 || !(options.allFiles === true || isFrontendSource(context))) return;
1060
- for (const comment of comments) context.report({ node: comment, messageId: "noFrontendComments" });
1073
+ const ranges = ambiguousJSX ? /* @__PURE__ */ new Map() : emptyCommentRanges(context.sourceCode.text, comments, jsxRanges);
1074
+ for (const comment of comments) context.report({
1075
+ node: comment,
1076
+ messageId: "noFrontendComments",
1077
+ fix(fixer) {
1078
+ const range = ranges.get(comment);
1079
+ return range === void 0 ? null : fixer.replaceTextRange([range.start, range.end], " ");
1080
+ }
1081
+ });
1061
1082
  }
1062
1083
  };
1063
1084
  }
1064
1085
  });
1086
+ function emptyCommentRanges(text, comments, jsxRanges) {
1087
+ const ranges = /* @__PURE__ */ new Map();
1088
+ let previousEnd = 0;
1089
+ let jsxIndex = 0;
1090
+ for (const comment of comments) {
1091
+ const range = sourceRange(comment.range, text.length);
1092
+ if (range === null || range.start < previousEnd) {
1093
+ ranges.clear();
1094
+ return ranges;
1095
+ }
1096
+ previousEnd = range.end;
1097
+ let jsx = jsxRanges[jsxIndex];
1098
+ while (jsx !== void 0 && jsx.end <= range.start) {
1099
+ jsxIndex += 1;
1100
+ jsx = jsxRanges[jsxIndex];
1101
+ }
1102
+ if (jsx !== void 0 && range.start < jsx.end && range.end > jsx.start) continue;
1103
+ const empty = emptyCommentRange(text, comment);
1104
+ if (empty !== null) ranges.set(comment, empty);
1105
+ }
1106
+ return ranges;
1107
+ }
1108
+ function sourceRange(range, length) {
1109
+ const [start, end] = range;
1110
+ return Number.isInteger(start) && Number.isInteger(end) && start >= 0 && end > start && end <= length ? { start, end } : null;
1111
+ }
1112
+ function emptyCommentRange(text, comment) {
1113
+ const range = sourceRange(comment.range, text.length);
1114
+ if (range === null || comment.value.trim() !== "") return null;
1115
+ const { start, end } = range;
1116
+ const raw = text.slice(start, end);
1117
+ if (/[\r\n\u2028\u2029]/u.test(raw)) return null;
1118
+ if (comment.type === "Line" && raw.startsWith("//") && raw.slice(2).trim() === "") return range;
1119
+ if (comment.type === "Block" && raw.startsWith("/*") && raw.endsWith("*/") && raw.slice(2, -2).trim() === "") return range;
1120
+ return null;
1121
+ }
1065
1122
 
1066
1123
  // src/rules/prefer-dependency-interface.ts
1067
1124
  var import_utils7 = require("@typescript-eslint/utils");
@@ -4644,7 +4701,7 @@ function isSearchParamReceiver(call, services) {
4644
4701
  // package.json
4645
4702
  var package_default = {
4646
4703
  name: "@sarj/eslint-plugin",
4647
- version: "17.1.0",
4704
+ version: "17.1.1",
4648
4705
  packageManager: "npm@12.2.1",
4649
4706
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
4650
4707
  type: "module",
package/dist/index.js CHANGED
@@ -113,28 +113,21 @@ function hasTypeInformation(services) {
113
113
  return services?.program != null && services.esTreeNodeToTSNodeMap != null && services.tsNodeToESTreeNodeMap != null;
114
114
  }
115
115
  function createNativeRule(config, url) {
116
- const input = { ...config };
117
- const originalMeta = Object.hasOwn(input, "meta") ? input.meta : config.meta;
116
+ const name = config.name;
117
+ const originalMeta = config.meta;
118
118
  const metadataSnapshot = { ...originalMeta };
119
119
  const declaredMetaDefaults = Object.hasOwn(metadataSnapshot, "defaultOptions") ? metadataSnapshot.defaultOptions : originalMeta.defaultOptions;
120
- const declaredLegacyDefaults = Object.hasOwn(input, "defaultOptions") ? input.defaultOptions : config.defaultOptions;
121
- const descriptor = {
122
- ...input,
123
- name: Object.hasOwn(input, "name") ? input.name : config.name,
124
- create: Object.hasOwn(input, "create") ? input.create : config.create,
125
- meta: metadataSnapshot
126
- };
127
- if (declaredLegacyDefaults !== void 0) descriptor.defaultOptions = declaredLegacyDefaults;
128
- if (declaredMetaDefaults !== void 0) descriptor.meta.defaultOptions = declaredMetaDefaults;
129
- const { defaultOptions: metaDefaults, schema, ...metadata } = descriptor.meta;
120
+ const declaredLegacyDefaults = config.defaultOptions;
121
+ if (declaredMetaDefaults !== void 0) metadataSnapshot.defaultOptions = declaredMetaDefaults;
122
+ const { defaultOptions: metaDefaults, schema, ...metadata } = metadataSnapshot;
130
123
  const metadataDefaults = metaDefaults === void 0 ? void 0 : ownTuple(metaDefaults);
131
- const legacyDefaults = metadataDefaults === void 0 && descriptor.defaultOptions !== void 0 ? ownTuple(descriptor.defaultOptions) : void 0;
124
+ const legacyDefaults = metadataDefaults === void 0 && declaredLegacyDefaults !== void 0 ? ownTuple(declaredLegacyDefaults) : void 0;
132
125
  const ownedMetadata = {
133
126
  ...metadata,
134
- docs: { ...metadata.docs, url: url(descriptor.name) },
127
+ docs: { ...metadata.docs, url: url(name) },
135
128
  schema: schemaList(schema) ? [...schema] : schema
136
129
  };
137
- if (metadataDefaults === void 0 && Object.hasOwn(descriptor.meta, "defaultOptions")) {
130
+ if (metadataDefaults === void 0 && Object.hasOwn(metadataSnapshot, "defaultOptions")) {
138
131
  Object.defineProperty(ownedMetadata, "defaultOptions", {
139
132
  value: void 0,
140
133
  configurable: true,
@@ -144,25 +137,25 @@ function createNativeRule(config, url) {
144
137
  }
145
138
  const meta2 = metadataDefaults === void 0 ? ownedMetadata : { ...ownedMetadata, defaultOptions: metadataDefaults };
146
139
  const base = {
147
- name: descriptor.name,
148
- defaultOptions: descriptor.defaultOptions,
140
+ name,
141
+ defaultOptions: declaredLegacyDefaults,
149
142
  meta: meta2
150
143
  };
151
- if (hasNoDefaultOptions(descriptor)) {
152
- const create2 = descriptor.create;
144
+ if (hasNoDefaultOptions(config, declaredMetaDefaults, declaredLegacyDefaults)) {
145
+ const create2 = config.create;
153
146
  return { ...base, create(context) {
154
147
  return create2(context, []);
155
148
  } };
156
149
  }
157
150
  const defaults = metadataDefaults ?? legacyDefaults;
158
151
  if (defaults === void 0) throw new TypeError("Declared defaults disappeared while creating the rule.");
159
- const create = descriptor.create;
152
+ const create = config.create;
160
153
  return { ...base, create(context) {
161
154
  return create(context, ESLintUtils.applyDefault(defaults, context.options));
162
155
  } };
163
156
  }
164
- function hasNoDefaultOptions(config) {
165
- return config.meta.defaultOptions === void 0 && config.defaultOptions === void 0;
157
+ function hasNoDefaultOptions(_config, metaDefaults, legacyDefaults) {
158
+ return metaDefaults === void 0 && legacyDefaults === void 0;
166
159
  }
167
160
  function schemaList(schema) {
168
161
  return Array.isArray(schema);
@@ -180,13 +173,12 @@ var KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
180
173
  var MAX_SUMMARY_LENGTH = 160;
181
174
  function createRule(config) {
182
175
  const { documentation } = config;
183
- const eslintConfig = config;
184
- const rule = createNativeRule(eslintConfig, examplesUrl);
176
+ const rule = createNativeRule(config, examplesUrl);
185
177
  if (documentation !== void 0) {
186
178
  Object.defineProperty(rule, "documentation", {
187
179
  configurable: false,
188
180
  enumerable: false,
189
- value: nativeSpec(eslintConfig, documentation),
181
+ value: nativeSpec(rule, documentation),
190
182
  writable: false
191
183
  });
192
184
  }
@@ -975,19 +967,22 @@ var NO_FRONTEND_COMMENTS_DOCUMENTATION = {
975
967
  defaultLevel: "warning",
976
968
  summary: "Express frontend behavior through code instead of prose comments.",
977
969
  rationale: "Frontend comments can drift from executable behavior and conceal names, types or structure that should communicate the same information.",
978
- remediation: "Use clear names, types, helpers and assertions, then remove the comment. Move tool settings into their owning configuration where supported. Review behavioral directives and legal notices before editing; no automatic deletion changes those contracts.",
970
+ remediation: "Use clear names, types, helpers and assertions, then remove the comment. Move tool settings into their owning configuration where supported. Whitespace-only single-line comments can be replaced with a space; review all other comments and their contracts manually.",
979
971
  category: "maintainability",
980
- autofix: "none",
972
+ autofix: "safe",
981
973
  limitations: [
982
974
  "Reports every actual line, block, JSDoc and JSX comment in maintained frontend source, including legal notices and executable directives. The existing no-test-comments rule owns maintained test files and applies the same blanket frontend ban, avoiding duplicate diagnostics between these two rules. Strings, template data, rendered JSX text and generated/vendor sources are excluded.",
983
975
  "Frontend scope is established by JSX-capable extensions, use client/use server directives, framework imports, or the nearest package manifest declaring a frontend framework dependency. Pure helpers in those packages are included. Set allFiles: true on both this rule and no-test-comments in a frontend ESLint override for dependency-free or unsupported frameworks; file names alone do not infer application ownership.",
984
- "This is a blanket policy warning, including comments preserved by narrower comment-hygiene rules. Behavioral annotations, JavaScript type comments, required legal notices and tool directives require manual migration or an explicit policy exception; the rule never automatically deletes them."
976
+ "This is a blanket policy warning, including comments preserved by narrower comment-hygiene rules. Behavioral annotations, JavaScript type comments, required legal notices and tool directives require manual migration or an explicit policy exception; the rule never automatically deletes them.",
977
+ "Automatic fixes replace only whitespace-only line or block comment tokens containing no line terminators with one separating space. Every existing line terminator is preserved. Comments overlapping JSX regions, multiline comments, nonempty comments and comments owned by the test rule remain manual."
985
978
  ],
986
979
  examples: [
987
980
  { id: "frontend-prose", title: "Express view behavior in code", outcome: "match", files: [{ path: "src/view.tsx", source: "// Show current status\nexport const View = () => <p>Ready</p>;" }], focusPath: "src/view.tsx", expectedCount: 1, public: true },
988
981
  { id: "named-view", title: "The view expresses its behavior", outcome: "no-match", files: [{ path: "src/view.tsx", source: "export const View = () => <p>Ready</p>;" }], focusPath: "src/view.tsx", expectedCount: 0, public: true },
989
982
  { id: "package-helper", scenarioId: "package", title: "A plain helper belongs to its frontend package", outcome: "match", files: [{ path: "package.json", source: '{"private":true,"dependencies":{"react":"19.2.0"}}' }, { path: "src/helper.ts", source: "// Frontend helper\nexport const ready = true;" }], focusPath: "src/helper.ts", expectedCount: 1, public: true },
990
- { id: "package-helper-code", scenarioId: "package", title: "The helper expresses its behavior in code", outcome: "no-match", files: [{ path: "package.json", source: '{"private":true,"dependencies":{"react":"19.2.0"}}' }, { path: "src/helper.ts", source: "export const ready = true;" }], focusPath: "src/helper.ts", expectedCount: 0, public: true }
983
+ { id: "package-helper-code", scenarioId: "package", title: "The helper expresses its behavior in code", outcome: "no-match", files: [{ path: "package.json", source: '{"private":true,"dependencies":{"react":"19.2.0"}}' }, { path: "src/helper.ts", source: "export const ready = true;" }], focusPath: "src/helper.ts", expectedCount: 0, public: true },
984
+ { id: "empty-comment-separator", scenarioId: "empty-comment", title: "Keep a separating space when removing an empty comment", outcome: "match", files: [{ path: "src/helper.tsx", source: "export function identity(value: number) { return/**/value; }" }], fixedFiles: [{ path: "src/helper.tsx", source: "export function identity(value: number) { return value; }" }], focusPath: "src/helper.tsx", expectedCount: 1, public: true },
985
+ { id: "empty-comment-removed", scenarioId: "empty-comment", title: "Preserve the return expression without a comment", outcome: "no-match", files: [{ path: "src/helper.tsx", source: "export function identity(value: number) { return value; }" }], focusPath: "src/helper.tsx", expectedCount: 0, public: true }
991
986
  ]
992
987
  };
993
988
  var no_frontend_comments_default = createRule({
@@ -995,6 +990,7 @@ var no_frontend_comments_default = createRule({
995
990
  documentation: NO_FRONTEND_COMMENTS_DOCUMENTATION,
996
991
  meta: {
997
992
  type: "suggestion",
993
+ fixable: "code",
998
994
  docs: { description: NO_FRONTEND_COMMENTS_DOCUMENTATION.summary },
999
995
  schema: [{ type: "object", properties: { allFiles: { type: "boolean" } }, additionalProperties: false }],
1000
996
  messages: { noFrontendComments: "Frontend comments are prohibited; express the behavior in names, types or code and review any directive or legal contract before removing it." }
@@ -1002,15 +998,76 @@ var no_frontend_comments_default = createRule({
1002
998
  defaultOptions: [{}],
1003
999
  create(context, [options]) {
1004
1000
  if (isGeneratedFile(context, context.sourceCode.text) || ownsTestComments(context)) return {};
1001
+ const jsxRanges = [];
1002
+ let ambiguousJSX = false;
1003
+ function captureJSX(node) {
1004
+ const range = sourceRange(node.range, context.sourceCode.text.length);
1005
+ if (range === null) {
1006
+ ambiguousJSX = true;
1007
+ return;
1008
+ }
1009
+ const previous = jsxRanges.at(-1);
1010
+ if (previous === void 0 || range.start >= previous.end) {
1011
+ jsxRanges.push(range);
1012
+ return;
1013
+ }
1014
+ if (range.start < previous.start || range.end > previous.end) ambiguousJSX = true;
1015
+ }
1005
1016
  return {
1006
- Program() {
1017
+ JSXElement: captureJSX,
1018
+ JSXFragment: captureJSX,
1019
+ "Program:exit"() {
1007
1020
  const comments = context.sourceCode.getAllComments();
1008
1021
  if (comments.length === 0 || !(options.allFiles === true || isFrontendSource(context))) return;
1009
- for (const comment of comments) context.report({ node: comment, messageId: "noFrontendComments" });
1022
+ const ranges = ambiguousJSX ? /* @__PURE__ */ new Map() : emptyCommentRanges(context.sourceCode.text, comments, jsxRanges);
1023
+ for (const comment of comments) context.report({
1024
+ node: comment,
1025
+ messageId: "noFrontendComments",
1026
+ fix(fixer) {
1027
+ const range = ranges.get(comment);
1028
+ return range === void 0 ? null : fixer.replaceTextRange([range.start, range.end], " ");
1029
+ }
1030
+ });
1010
1031
  }
1011
1032
  };
1012
1033
  }
1013
1034
  });
1035
+ function emptyCommentRanges(text, comments, jsxRanges) {
1036
+ const ranges = /* @__PURE__ */ new Map();
1037
+ let previousEnd = 0;
1038
+ let jsxIndex = 0;
1039
+ for (const comment of comments) {
1040
+ const range = sourceRange(comment.range, text.length);
1041
+ if (range === null || range.start < previousEnd) {
1042
+ ranges.clear();
1043
+ return ranges;
1044
+ }
1045
+ previousEnd = range.end;
1046
+ let jsx = jsxRanges[jsxIndex];
1047
+ while (jsx !== void 0 && jsx.end <= range.start) {
1048
+ jsxIndex += 1;
1049
+ jsx = jsxRanges[jsxIndex];
1050
+ }
1051
+ if (jsx !== void 0 && range.start < jsx.end && range.end > jsx.start) continue;
1052
+ const empty = emptyCommentRange(text, comment);
1053
+ if (empty !== null) ranges.set(comment, empty);
1054
+ }
1055
+ return ranges;
1056
+ }
1057
+ function sourceRange(range, length) {
1058
+ const [start, end] = range;
1059
+ return Number.isInteger(start) && Number.isInteger(end) && start >= 0 && end > start && end <= length ? { start, end } : null;
1060
+ }
1061
+ function emptyCommentRange(text, comment) {
1062
+ const range = sourceRange(comment.range, text.length);
1063
+ if (range === null || comment.value.trim() !== "") return null;
1064
+ const { start, end } = range;
1065
+ const raw = text.slice(start, end);
1066
+ if (/[\r\n\u2028\u2029]/u.test(raw)) return null;
1067
+ if (comment.type === "Line" && raw.startsWith("//") && raw.slice(2).trim() === "") return range;
1068
+ if (comment.type === "Block" && raw.startsWith("/*") && raw.endsWith("*/") && raw.slice(2, -2).trim() === "") return range;
1069
+ return null;
1070
+ }
1014
1071
 
1015
1072
  // src/rules/prefer-dependency-interface.ts
1016
1073
  import { AST_NODE_TYPES as AST_NODE_TYPES5, ASTUtils } from "@typescript-eslint/utils";
@@ -4593,7 +4650,7 @@ function isSearchParamReceiver(call, services) {
4593
4650
  // package.json
4594
4651
  var package_default = {
4595
4652
  name: "@sarj/eslint-plugin",
4596
- version: "17.1.0",
4653
+ version: "17.1.1",
4597
4654
  packageManager: "npm@12.2.1",
4598
4655
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
4599
4656
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "17.1.0",
3
+ "version": "17.1.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",