@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 +89 -32
- package/dist/index.js +89 -32
- package/package.json +1 -1
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
|
|
168
|
-
const originalMeta =
|
|
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 =
|
|
172
|
-
|
|
173
|
-
|
|
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 &&
|
|
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(
|
|
178
|
+
docs: { ...metadata.docs, url: url(name) },
|
|
186
179
|
schema: schemaList(schema) ? [...schema] : schema
|
|
187
180
|
};
|
|
188
|
-
if (metadataDefaults === void 0 && Object.hasOwn(
|
|
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
|
|
199
|
-
defaultOptions:
|
|
191
|
+
name,
|
|
192
|
+
defaultOptions: declaredLegacyDefaults,
|
|
200
193
|
meta: meta2
|
|
201
194
|
};
|
|
202
|
-
if (hasNoDefaultOptions(
|
|
203
|
-
const create2 =
|
|
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 =
|
|
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(
|
|
216
|
-
return
|
|
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
|
|
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(
|
|
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.
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
117
|
-
const originalMeta =
|
|
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 =
|
|
121
|
-
|
|
122
|
-
|
|
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 &&
|
|
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(
|
|
127
|
+
docs: { ...metadata.docs, url: url(name) },
|
|
135
128
|
schema: schemaList(schema) ? [...schema] : schema
|
|
136
129
|
};
|
|
137
|
-
if (metadataDefaults === void 0 && Object.hasOwn(
|
|
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
|
|
148
|
-
defaultOptions:
|
|
140
|
+
name,
|
|
141
|
+
defaultOptions: declaredLegacyDefaults,
|
|
149
142
|
meta: meta2
|
|
150
143
|
};
|
|
151
|
-
if (hasNoDefaultOptions(
|
|
152
|
-
const create2 =
|
|
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 =
|
|
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(
|
|
165
|
-
return
|
|
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
|
|
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(
|
|
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.
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|