@kazupon/eslint-plugin 0.3.0 → 0.4.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/README.md +5 -4
- package/lib/index.d.ts +2 -1
- package/lib/index.js +232 -46
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -61,10 +61,11 @@ The rules with the following star ⭐ are included in the configs.
|
|
|
61
61
|
|
|
62
62
|
### @kazupon/eslint-plugin Rules
|
|
63
63
|
|
|
64
|
-
| Rule ID
|
|
65
|
-
|
|
|
66
|
-
| [@kazupon/enforce-header-comment](https://eslint-plugin.kazupon.dev/rules/enforce-header-comment.html)
|
|
67
|
-
| [@kazupon/no-tag-comments](https://eslint-plugin.kazupon.dev/rules/no-tag-comments.html)
|
|
64
|
+
| Rule ID | Description | Category | Fixable | RECOMMENDED |
|
|
65
|
+
| :--------------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | :------- | :-----: | :---------: |
|
|
66
|
+
| [@kazupon/enforce-header-comment](https://eslint-plugin.kazupon.dev/rules/enforce-header-comment.html) | Enforce heading the comment in source code file | Comment | | ⭐ |
|
|
67
|
+
| [@kazupon/no-tag-comments](https://eslint-plugin.kazupon.dev/rules/no-tag-comments.html) | disallow tag comments | Comment | | ⭐ |
|
|
68
|
+
| [@kazupon/prefer-scope-on-tag-comment](https://eslint-plugin.kazupon.dev/rules/prefer-scope-on-tag-comment.html) | enforce adding a scope to tag comments | Comment | | ⭐ |
|
|
68
69
|
|
|
69
70
|
<!--RULES_TABLE_END-->
|
|
70
71
|
|
package/lib/index.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ type PluginConfigs = {
|
|
|
8
8
|
declare const plugin: Omit<ESLint.Plugin, "configs"> & {
|
|
9
9
|
configs: PluginConfigs;
|
|
10
10
|
};
|
|
11
|
+
declare const recommendedConfig: Linter.Config[];
|
|
11
12
|
declare const commentConfig: Linter.Config[];
|
|
12
13
|
declare const configs: {
|
|
13
|
-
recommended: typeof
|
|
14
|
+
recommended: typeof recommendedConfig;
|
|
14
15
|
comment: typeof commentConfig;
|
|
15
16
|
};
|
|
16
17
|
/** @alias */
|
package/lib/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const name = "@kazupon/eslint-plugin";
|
|
|
12
12
|
/**
|
|
13
13
|
* The plugin version.
|
|
14
14
|
*/
|
|
15
|
-
const version = "0.
|
|
15
|
+
const version = "0.4.0";
|
|
16
16
|
/**
|
|
17
17
|
* The namespace for rules
|
|
18
18
|
*/
|
|
@@ -22,7 +22,7 @@ const namespace = "@kazupon";
|
|
|
22
22
|
//#region src/utils/rule.ts
|
|
23
23
|
const BLOB_URL = "https://eslint-plugin.kazupon.dev/rules";
|
|
24
24
|
function RuleCreator(urlCreator, namespace$1 = "") {
|
|
25
|
-
return function createNamedRule({ meta, name: name$1,...rule$
|
|
25
|
+
return function createNamedRule({ meta, name: name$1,...rule$3 }) {
|
|
26
26
|
const ruleId = namespace$1 ? `${namespace$1}/${name$1}` : name$1;
|
|
27
27
|
return {
|
|
28
28
|
meta: {
|
|
@@ -34,7 +34,7 @@ function RuleCreator(urlCreator, namespace$1 = "") {
|
|
|
34
34
|
ruleId
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
-
...rule$
|
|
37
|
+
...rule$3
|
|
38
38
|
};
|
|
39
39
|
};
|
|
40
40
|
}
|
|
@@ -53,7 +53,7 @@ function initializeTagDiagnosis(tags) {
|
|
|
53
53
|
function validTagDiagnosis(tagDiagnosis) {
|
|
54
54
|
return Object.keys(tagDiagnosis).every((tag) => tagDiagnosis[tag] === "ok");
|
|
55
55
|
}
|
|
56
|
-
const rule$
|
|
56
|
+
const rule$2 = createRule({
|
|
57
57
|
name: "enforce-header-comment",
|
|
58
58
|
meta: {
|
|
59
59
|
type: "suggestion",
|
|
@@ -181,11 +181,10 @@ const rule$1 = createRule({
|
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
});
|
|
184
|
-
var enforce_header_comment_default = rule$
|
|
184
|
+
var enforce_header_comment_default = rule$2;
|
|
185
185
|
|
|
186
186
|
//#endregion
|
|
187
|
-
//#region src/
|
|
188
|
-
const DEFAULT_TAGS = ["FIXME", "BUG"];
|
|
187
|
+
//#region src/utils/comment.ts
|
|
189
188
|
/**
|
|
190
189
|
* Remove JSDoc asterisk prefix if present
|
|
191
190
|
*/
|
|
@@ -193,17 +192,77 @@ function stripJSDocPrefix(line) {
|
|
|
193
192
|
const trimmed = line.trim();
|
|
194
193
|
return trimmed.startsWith("*") ? trimmed.slice(1).trim() : trimmed;
|
|
195
194
|
}
|
|
196
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Check if the text starts with any of the given tags
|
|
197
|
+
* @param text The text to check
|
|
198
|
+
* @param tags Array of tags to search for
|
|
199
|
+
* @returns Tag detection result or null if no tag found
|
|
200
|
+
*/
|
|
201
|
+
function detectTag(text, tags) {
|
|
202
|
+
for (const tag of tags) {
|
|
203
|
+
const tagRegex = new RegExp(`^${tag}\\b`);
|
|
204
|
+
const match = text.match(tagRegex);
|
|
205
|
+
if (match) {
|
|
206
|
+
const afterTag = text.slice(tag.length);
|
|
207
|
+
if (afterTag.startsWith("(")) {
|
|
208
|
+
const closingParenIndex = afterTag.indexOf(")");
|
|
209
|
+
if (closingParenIndex > 0) {
|
|
210
|
+
const scope = afterTag.slice(1, closingParenIndex).trim();
|
|
211
|
+
return {
|
|
212
|
+
tag,
|
|
213
|
+
hasScope: scope.length > 0
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
tag,
|
|
218
|
+
hasScope: false
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (afterTag === "" || afterTag.startsWith(":") || afterTag.startsWith(" ")) return {
|
|
222
|
+
tag,
|
|
223
|
+
hasScope: false
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Calculate the exact location of a tag in a comment
|
|
231
|
+
* @param comment The comment containing the tag
|
|
232
|
+
* @param line The line of text containing the tag
|
|
233
|
+
* @param lineIndex The index of the line within the comment
|
|
234
|
+
* @param tag The tag to locate
|
|
235
|
+
* @returns Location with line and column, or null if not found
|
|
236
|
+
*/
|
|
237
|
+
function calculateTagLocation(comment, line, lineIndex, tag) {
|
|
238
|
+
const tagIndex = line.indexOf(tag);
|
|
239
|
+
if (tagIndex === -1) return null;
|
|
240
|
+
if (lineIndex === 0) {
|
|
241
|
+
const tagIndexInValue = comment.value.indexOf(tag);
|
|
242
|
+
return tagIndexInValue === -1 ? null : {
|
|
243
|
+
line: comment.loc.start.line,
|
|
244
|
+
column: comment.loc.start.column + 2 + tagIndexInValue
|
|
245
|
+
};
|
|
246
|
+
} else return {
|
|
247
|
+
line: comment.loc.start.line + lineIndex,
|
|
248
|
+
column: tagIndex
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/rules/no-tag-comments.ts
|
|
254
|
+
const DEFAULT_TAGS$1 = ["FIXME", "BUG"];
|
|
255
|
+
const rule$1 = createRule({
|
|
197
256
|
name: "no-tag-comments",
|
|
198
257
|
meta: {
|
|
199
258
|
type: "problem",
|
|
200
259
|
docs: {
|
|
201
260
|
description: "disallow tag comments",
|
|
202
261
|
category: "Comment",
|
|
203
|
-
recommended:
|
|
262
|
+
recommended: true,
|
|
204
263
|
defaultSeverity: "warn"
|
|
205
264
|
},
|
|
206
|
-
messages: {
|
|
265
|
+
messages: { tagComment: "Exist '{{tag}}' tag comment" },
|
|
207
266
|
schema: [{
|
|
208
267
|
type: "object",
|
|
209
268
|
properties: { tags: {
|
|
@@ -216,59 +275,168 @@ const rule = createRule({
|
|
|
216
275
|
}]
|
|
217
276
|
},
|
|
218
277
|
create(ctx) {
|
|
219
|
-
const options = ctx.options[0] || { tags: DEFAULT_TAGS };
|
|
220
|
-
const tags = options.tags || DEFAULT_TAGS;
|
|
278
|
+
const options = ctx.options[0] || { tags: DEFAULT_TAGS$1 };
|
|
279
|
+
const tags = options.tags || DEFAULT_TAGS$1;
|
|
221
280
|
const sourceCode = ctx.sourceCode;
|
|
222
281
|
/**
|
|
223
|
-
* Check if the text starts with a tag followed by valid delimiter
|
|
224
|
-
*/
|
|
225
|
-
function hasTag(text) {
|
|
226
|
-
for (const tag of tags) if (text.startsWith(tag)) {
|
|
227
|
-
const afterTag = text.slice(tag.length);
|
|
228
|
-
if (afterTag === "" || afterTag.startsWith(":") || afterTag.startsWith(" ")) return tag;
|
|
229
|
-
}
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
282
|
* Report a tag comment violation
|
|
234
283
|
*/
|
|
235
|
-
function reportTag(comment, tag) {
|
|
236
|
-
ctx.report({
|
|
237
|
-
messageId: "
|
|
284
|
+
function reportTag(comment, tag, loc) {
|
|
285
|
+
if (loc && comment.loc) ctx.report({
|
|
286
|
+
messageId: "tagComment",
|
|
287
|
+
data: { tag },
|
|
288
|
+
loc: {
|
|
289
|
+
start: {
|
|
290
|
+
line: loc.line,
|
|
291
|
+
column: loc.column
|
|
292
|
+
},
|
|
293
|
+
end: {
|
|
294
|
+
line: loc.line,
|
|
295
|
+
column: loc.column + tag.length
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
else ctx.report({
|
|
300
|
+
messageId: "tagComment",
|
|
238
301
|
data: { tag },
|
|
239
302
|
loc: comment.loc
|
|
240
303
|
});
|
|
241
304
|
}
|
|
242
305
|
/**
|
|
243
|
-
*
|
|
306
|
+
* Check a comment for tag violations
|
|
244
307
|
*/
|
|
245
|
-
function
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
308
|
+
function checkComment(comment) {
|
|
309
|
+
const { value, type } = comment;
|
|
310
|
+
if (type === "Line") {
|
|
311
|
+
const tagInfo = detectTag(value.trim(), tags);
|
|
312
|
+
if (tagInfo) {
|
|
313
|
+
const tagIndex = value.indexOf(tagInfo.tag);
|
|
314
|
+
if (tagIndex !== -1) reportTag(comment, tagInfo.tag, {
|
|
315
|
+
line: comment.loc.start.line,
|
|
316
|
+
column: comment.loc.start.column + 2 + tagIndex
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const lines = value.split("\n");
|
|
322
|
+
for (const [i, line] of lines.entries()) {
|
|
323
|
+
const trimmedLine = line.trim();
|
|
324
|
+
if (!trimmedLine) continue;
|
|
325
|
+
const contentToCheck = stripJSDocPrefix(line);
|
|
326
|
+
const tagInfo = detectTag(contentToCheck, tags);
|
|
327
|
+
if (tagInfo) {
|
|
328
|
+
const location = calculateTagLocation(comment, line, i, tagInfo.tag);
|
|
329
|
+
if (location) {
|
|
330
|
+
reportTag(comment, tagInfo.tag, location);
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
250
334
|
}
|
|
251
|
-
return false;
|
|
252
335
|
}
|
|
336
|
+
return { Program() {
|
|
337
|
+
const comments = sourceCode.getAllComments();
|
|
338
|
+
for (const comment of comments) checkComment(comment);
|
|
339
|
+
} };
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
var no_tag_comments_default = rule$1;
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/rules/prefer-scope-on-tag-comment.ts
|
|
346
|
+
const DEFAULT_TAGS = [
|
|
347
|
+
"TODO",
|
|
348
|
+
"FIXME",
|
|
349
|
+
"HACK",
|
|
350
|
+
"BUG",
|
|
351
|
+
"NOTE"
|
|
352
|
+
];
|
|
353
|
+
const rule = createRule({
|
|
354
|
+
name: "prefer-scope-on-tag-comment",
|
|
355
|
+
meta: {
|
|
356
|
+
type: "suggestion",
|
|
357
|
+
docs: {
|
|
358
|
+
description: "enforce adding a scope to tag comments",
|
|
359
|
+
category: "Comment",
|
|
360
|
+
recommended: true,
|
|
361
|
+
defaultSeverity: "warn"
|
|
362
|
+
},
|
|
363
|
+
messages: { missingScope: "Tag comment '{{tag}}' is missing a scope. Use format: {{tag}}(scope)" },
|
|
364
|
+
schema: [{
|
|
365
|
+
type: "object",
|
|
366
|
+
properties: { tags: {
|
|
367
|
+
type: "array",
|
|
368
|
+
items: { type: "string" },
|
|
369
|
+
minItems: 1,
|
|
370
|
+
uniqueItems: true
|
|
371
|
+
} },
|
|
372
|
+
additionalProperties: false
|
|
373
|
+
}]
|
|
374
|
+
},
|
|
375
|
+
create(ctx) {
|
|
376
|
+
const options = ctx.options[0] || { tags: DEFAULT_TAGS };
|
|
377
|
+
const tags = options.tags || DEFAULT_TAGS;
|
|
378
|
+
const sourceCode = ctx.sourceCode;
|
|
253
379
|
/**
|
|
254
|
-
*
|
|
380
|
+
* Report a missing scope violation
|
|
381
|
+
*/
|
|
382
|
+
function reportMissingScope(comment, tag, loc) {
|
|
383
|
+
if (!comment.loc) {
|
|
384
|
+
ctx.report({
|
|
385
|
+
messageId: "missingScope",
|
|
386
|
+
data: { tag },
|
|
387
|
+
node: ctx.sourceCode.ast
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (loc && comment.loc) ctx.report({
|
|
392
|
+
messageId: "missingScope",
|
|
393
|
+
data: { tag },
|
|
394
|
+
loc: {
|
|
395
|
+
start: {
|
|
396
|
+
line: loc.line,
|
|
397
|
+
column: loc.column
|
|
398
|
+
},
|
|
399
|
+
end: {
|
|
400
|
+
line: loc.line,
|
|
401
|
+
column: loc.column + tag.length
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
else ctx.report({
|
|
406
|
+
messageId: "missingScope",
|
|
407
|
+
data: { tag },
|
|
408
|
+
loc: comment.loc
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Check a comment for missing scope
|
|
255
413
|
*/
|
|
256
414
|
function checkComment(comment) {
|
|
257
415
|
const { value, type } = comment;
|
|
258
416
|
if (type === "Line") {
|
|
259
|
-
|
|
417
|
+
const tagInfo = detectTag(value.trim(), tags);
|
|
418
|
+
if (tagInfo && !tagInfo.hasScope) {
|
|
419
|
+
const tagIndex = value.indexOf(tagInfo.tag);
|
|
420
|
+
if (tagIndex !== -1) reportMissingScope(comment, tagInfo.tag, {
|
|
421
|
+
line: comment.loc.start.line,
|
|
422
|
+
column: comment.loc.start.column + 2 + tagIndex
|
|
423
|
+
});
|
|
424
|
+
}
|
|
260
425
|
return;
|
|
261
426
|
}
|
|
262
427
|
const lines = value.split("\n");
|
|
263
|
-
|
|
264
|
-
checkLine(value.trim(), comment);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
for (const line of lines) {
|
|
428
|
+
for (const [i, line] of lines.entries()) {
|
|
268
429
|
const trimmedLine = line.trim();
|
|
269
430
|
if (!trimmedLine) continue;
|
|
270
431
|
const contentToCheck = stripJSDocPrefix(line);
|
|
271
|
-
|
|
432
|
+
const tagInfo = detectTag(contentToCheck, tags);
|
|
433
|
+
if (tagInfo && !tagInfo.hasScope) {
|
|
434
|
+
const location = calculateTagLocation(comment, line, i, tagInfo.tag);
|
|
435
|
+
if (location) {
|
|
436
|
+
reportMissingScope(comment, tagInfo.tag, location);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
272
440
|
}
|
|
273
441
|
}
|
|
274
442
|
return { Program() {
|
|
@@ -277,13 +445,14 @@ const rule = createRule({
|
|
|
277
445
|
} };
|
|
278
446
|
}
|
|
279
447
|
});
|
|
280
|
-
var
|
|
448
|
+
var prefer_scope_on_tag_comment_default = rule;
|
|
281
449
|
|
|
282
450
|
//#endregion
|
|
283
451
|
//#region src/rules/index.ts
|
|
284
452
|
const rules = {
|
|
285
453
|
"enforce-header-comment": enforce_header_comment_default,
|
|
286
|
-
"no-tag-comments": no_tag_comments_default
|
|
454
|
+
"no-tag-comments": no_tag_comments_default,
|
|
455
|
+
"prefer-scope-on-tag-comment": prefer_scope_on_tag_comment_default
|
|
287
456
|
};
|
|
288
457
|
|
|
289
458
|
//#endregion
|
|
@@ -296,6 +465,23 @@ const plugin = {
|
|
|
296
465
|
rules,
|
|
297
466
|
configs: {}
|
|
298
467
|
};
|
|
468
|
+
const recommendedConfig = [{
|
|
469
|
+
name: "@kazupon/eslint-plugin/recommended",
|
|
470
|
+
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
471
|
+
ignores: [
|
|
472
|
+
"**/*.md",
|
|
473
|
+
"**/*.md/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
|
|
474
|
+
"**/*.config.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
|
|
475
|
+
],
|
|
476
|
+
plugins: { [namespace]: plugin },
|
|
477
|
+
rules: Object.entries(rules).reduce((acc, [ruleName, rule$3]) => {
|
|
478
|
+
if (rule$3.meta?.docs?.recommended) {
|
|
479
|
+
const ruleId = rule$3.meta?.docs?.ruleId || (namespace ? `${namespace}/${ruleName}` : ruleName);
|
|
480
|
+
acc[ruleId] = rule$3.meta?.docs?.defaultSeverity || "warn";
|
|
481
|
+
}
|
|
482
|
+
return acc;
|
|
483
|
+
}, Object.create(null))
|
|
484
|
+
}];
|
|
299
485
|
const commentConfig = [{
|
|
300
486
|
name: "@kazupon/eslint-plugin/comment",
|
|
301
487
|
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
@@ -305,14 +491,14 @@ const commentConfig = [{
|
|
|
305
491
|
"**/*.config.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
|
|
306
492
|
],
|
|
307
493
|
plugins: { [namespace]: plugin },
|
|
308
|
-
rules: Object.entries(rules).reduce((rules$1, [ruleName, rule$
|
|
309
|
-
const ruleId = rule$
|
|
310
|
-
rules$1[ruleId] = rule$
|
|
494
|
+
rules: Object.entries(rules).reduce((rules$1, [ruleName, rule$3]) => {
|
|
495
|
+
const ruleId = rule$3.meta?.docs?.ruleId || (namespace ? `${namespace}/${ruleName}` : ruleName);
|
|
496
|
+
rules$1[ruleId] = rule$3.meta?.docs?.defaultSeverity || "warn";
|
|
311
497
|
return rules$1;
|
|
312
498
|
}, Object.create(null))
|
|
313
499
|
}];
|
|
314
500
|
const configs = {
|
|
315
|
-
recommended:
|
|
501
|
+
recommended: recommendedConfig,
|
|
316
502
|
comment: commentConfig
|
|
317
503
|
};
|
|
318
504
|
plugin.configs = configs;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kazupon/eslint-plugin",
|
|
3
3
|
"description": "ESLint plugin for @kazupon",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"funding": "https://github.com/sponsors/kazupon",
|
|
7
7
|
"bugs": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@eslint/compat": "^1.2.9",
|
|
58
58
|
"@eslint/markdown": "^6.5.0",
|
|
59
|
-
"@kazupon/eslint-config": "^0.
|
|
59
|
+
"@kazupon/eslint-config": "^0.31.0",
|
|
60
60
|
"@kazupon/prettier-config": "^0.1.1",
|
|
61
61
|
"@shikijs/vitepress-twoslash": "^3.6.0",
|
|
62
62
|
"@types/node": "^22.15.30",
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
"eslint-plugin-jsonc": "^2.20.1",
|
|
70
70
|
"eslint-plugin-module-interop": "^0.3.1",
|
|
71
71
|
"eslint-plugin-promise": "^7.2.1",
|
|
72
|
-
"eslint-plugin-
|
|
72
|
+
"eslint-plugin-regexp": "^2.8.0",
|
|
73
|
+
"eslint-plugin-unicorn": "^59.0.0",
|
|
73
74
|
"eslint-plugin-unused-imports": "^4.1.4",
|
|
74
75
|
"eslint-plugin-yml": "^1.18.0",
|
|
75
76
|
"eslint-vitest-rule-tester": "^2.2.0",
|