@tony.ganchev/eslint-plugin-header 3.2.0 → 3.2.2
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 +210 -77
- package/index.d.ts +29 -0
- package/index.js +7 -5
- package/lib/comment-parser.js +1 -1
- package/lib/rules/eslint-utils.js +2 -1
- package/lib/rules/header.docs.js +1 -12
- package/lib/rules/header.js +256 -145
- package/lib/rules/header.schema.js +1 -0
- package/package.json +55 -14
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/lib/comment-parser.d.ts +3 -0
- package/types/lib/comment-parser.d.ts.map +1 -0
- package/types/lib/rules/eslint-utils.d.ts +10 -0
- package/types/lib/rules/eslint-utils.d.ts.map +1 -0
- package/types/lib/rules/header.d.ts +100 -0
- package/types/lib/rules/header.d.ts.map +1 -0
- package/types/lib/rules/header.docs.d.ts +3 -0
- package/types/lib/rules/header.docs.d.ts.map +1 -0
- package/types/lib/rules/header.schema.d.ts +20 -0
- package/types/lib/rules/header.schema.d.ts.map +1 -0
package/lib/rules/header.js
CHANGED
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
|
|
25
25
|
"use strict";
|
|
26
26
|
|
|
27
|
-
const assert = require("assert");
|
|
28
|
-
const fs = require("fs");
|
|
29
|
-
const os = require("os");
|
|
27
|
+
const assert = require("node:assert");
|
|
28
|
+
const fs = require("node:fs");
|
|
29
|
+
const os = require("node:os");
|
|
30
30
|
const commentParser = require("../comment-parser");
|
|
31
31
|
const { contextSourceCode } = require("./eslint-utils");
|
|
32
|
-
const { description, recommended
|
|
32
|
+
const { description, recommended } = require("./header.docs");
|
|
33
33
|
const { lineEndingOptions, commentTypeOptions, schema } = require("./header.schema");
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -38,8 +38,6 @@ const { lineEndingOptions, commentTypeOptions, schema } = require("./header.sche
|
|
|
38
38
|
* @typedef {import('eslint').Rule.NodeListener} NodeListener
|
|
39
39
|
* @typedef {import('eslint').Rule.ReportFixer} ReportFixer
|
|
40
40
|
* @typedef {import('eslint').Rule.RuleFixer} RuleFixer
|
|
41
|
-
* @typedef {import('eslint').Rule.RuleTextEdit} RuleTextEdit
|
|
42
|
-
* @typedef {import('eslint').Rule.RuleTextEditor} RuleTextEditor
|
|
43
41
|
* @typedef {import('eslint').Rule.RuleContext} RuleContext
|
|
44
42
|
* @typedef {import('estree').Comment} Comment
|
|
45
43
|
* @typedef {import('estree').Program} Program
|
|
@@ -50,61 +48,67 @@ const { lineEndingOptions, commentTypeOptions, schema } = require("./header.sche
|
|
|
50
48
|
* Local type definitions.
|
|
51
49
|
* @typedef {{ pattern: string | RegExp, template?: string }} HeaderLinePattern
|
|
52
50
|
* @typedef {string | RegExp | HeaderLinePattern} HeaderLine
|
|
53
|
-
* @typedef {
|
|
54
|
-
* @typedef {
|
|
55
|
-
* @typedef {
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* | [('block' | 'line'), HeaderLines ]
|
|
59
|
-
* | [('block' | 'line'), HeaderLines, HeaderSettings]
|
|
60
|
-
* | [('block' | 'line'), HeaderLines, number ]
|
|
61
|
-
* | [('block' | 'line'), HeaderLines, number, HeaderSettings]
|
|
62
|
-
* } LegacyHeaderOptions
|
|
63
|
-
* @typedef {
|
|
64
|
-
* {
|
|
51
|
+
* @typedef {HeaderLine | HeaderLine[]} HeaderLines
|
|
52
|
+
* @typedef {'os' | 'unix' | 'windows'} LineEndingOption
|
|
53
|
+
* @typedef {{ lineEndings?: LineEndingOption }} HeaderSettings
|
|
54
|
+
* @typedef {'block' | 'line'} CommentType
|
|
55
|
+
* @typedef {{
|
|
65
56
|
* file: string,
|
|
66
|
-
* encoding?:
|
|
57
|
+
* encoding?: BufferEncoding
|
|
67
58
|
* }
|
|
68
59
|
* } FileBasedConfig
|
|
69
|
-
* @typedef {
|
|
70
|
-
*
|
|
71
|
-
* commentType: 'line' | 'block',
|
|
60
|
+
* @typedef {{
|
|
61
|
+
* commentType: CommentType,
|
|
72
62
|
* lines: HeaderLine[]
|
|
73
63
|
* }
|
|
74
|
-
* }
|
|
64
|
+
* } InlineConfig
|
|
75
65
|
* @typedef {{ minimum?: number }} TrailingEmptyLines
|
|
76
|
-
* @typedef {
|
|
77
|
-
*
|
|
78
|
-
* header: FileBasedConfig | LinesBasedConfig,
|
|
66
|
+
* @typedef {{
|
|
67
|
+
* header: FileBasedConfig | InlineConfig,
|
|
79
68
|
* trailingEmptyLines?: TrailingEmptyLines
|
|
80
69
|
* }
|
|
81
70
|
* & HeaderSettings
|
|
82
|
-
* }
|
|
83
|
-
* @typedef {
|
|
71
|
+
* } HeaderOptions
|
|
72
|
+
* @typedef {[HeaderOptions] |
|
|
73
|
+
* [template: string] |
|
|
74
|
+
* [template: string, settings: HeaderSettings] |
|
|
75
|
+
* [type: CommentType, lines: HeaderLines] |
|
|
76
|
+
* [type: CommentType, lines: HeaderLines, settings: HeaderSettings] |
|
|
77
|
+
* [type: CommentType, lines: HeaderLines, minLines: number] |
|
|
78
|
+
* [
|
|
79
|
+
* type: CommentType,
|
|
80
|
+
* lines: HeaderLines,
|
|
81
|
+
* minLines: number,
|
|
82
|
+
* settings: HeaderSettings
|
|
83
|
+
* ]
|
|
84
|
+
* } AllHeaderOptions
|
|
85
|
+
* @typedef {import('eslint').Linter.RuleEntry<AllHeaderOptions>
|
|
86
|
+
* } HeaderRuleConfig
|
|
84
87
|
*/
|
|
85
88
|
|
|
86
89
|
/**
|
|
87
90
|
* Tests if the passed line configuration string or object is a pattern
|
|
88
91
|
* definition.
|
|
89
92
|
* @param {HeaderLine} object line configuration object or string
|
|
90
|
-
* @returns {
|
|
91
|
-
*
|
|
93
|
+
* @returns {object is HeaderLinePattern} `true` if the line configuration is a
|
|
94
|
+
* pattern-defining object or `false`
|
|
95
|
+
* otherwise.
|
|
92
96
|
*/
|
|
93
97
|
function isPattern(object) {
|
|
94
98
|
return typeof object === "object"
|
|
95
|
-
&& (
|
|
99
|
+
&& (Object.prototype.hasOwnProperty.call(object, "pattern"));
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
/**
|
|
99
103
|
* Utility over a line config argument to match an expected string either
|
|
100
104
|
* against a regex or for full match against a string.
|
|
101
|
-
* @param {
|
|
102
|
-
* @param {string} expected The string or regex to test again.
|
|
105
|
+
* @param {string} actual the string to test.
|
|
106
|
+
* @param {string | RegExp} expected The string or regex to test again.
|
|
103
107
|
* @returns {boolean} `true` if the passed string matches the expected line
|
|
104
108
|
* config or `false` otherwise.
|
|
105
109
|
*/
|
|
106
110
|
function match(actual, expected) {
|
|
107
|
-
if (expected
|
|
111
|
+
if (expected instanceof RegExp) {
|
|
108
112
|
return expected.test(actual);
|
|
109
113
|
} else {
|
|
110
114
|
return expected === actual;
|
|
@@ -113,35 +117,62 @@ function match(actual, expected) {
|
|
|
113
117
|
|
|
114
118
|
/**
|
|
115
119
|
* Remove Unix she-bangs from the list of comments.
|
|
116
|
-
* @param {Comment[]} comments the list of comment
|
|
120
|
+
* @param {(Comment | { type: "Shebang" })[]} comments the list of comment
|
|
121
|
+
* lines.
|
|
117
122
|
* @returns {Comment[]} the list of comments with containing all incoming
|
|
118
123
|
* comments from `comments` with the shebang comments
|
|
119
124
|
* omitted.
|
|
120
125
|
*/
|
|
121
126
|
function excludeShebangs(comments) {
|
|
127
|
+
/** @type {Comment[]} */
|
|
122
128
|
return comments.filter(function(comment) {
|
|
123
129
|
return comment.type !== "Shebang";
|
|
124
130
|
});
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
/**
|
|
134
|
+
* TypeScript helper to confirm defined type.
|
|
135
|
+
* @template T
|
|
136
|
+
* @param {T | undefined} val the value to validate.
|
|
137
|
+
* @returns {asserts val is T} validates defined type
|
|
138
|
+
*/
|
|
139
|
+
function assertDefined(val) {
|
|
140
|
+
assert.strict.notEqual(typeof val, "undefined");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* TypeScript helper to confirm non-null type.
|
|
145
|
+
* @template T
|
|
146
|
+
* @param {T | null} val the value to validate.
|
|
147
|
+
* @returns {asserts val is T} validates non-null type
|
|
148
|
+
*/
|
|
149
|
+
function assertNotNull(val) {
|
|
150
|
+
assert.strict.notEqual(val, null);
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
/**
|
|
128
154
|
* Returns either the first block comment or the first set of line comments that
|
|
129
155
|
* are ONLY separated by a single newline. Note that this does not actually
|
|
130
156
|
* check if they are at the start of the file since that is already checked by
|
|
131
157
|
* `hasHeader()`.
|
|
132
158
|
* @param {RuleContext} context ESLint execution environment.
|
|
133
|
-
* @param {Program} node ESLint AST tree node being processed.
|
|
134
159
|
* @returns {Comment[]} lines that constitute the leading comment.
|
|
135
160
|
*/
|
|
136
|
-
function getLeadingComments(context
|
|
161
|
+
function getLeadingComments(context) {
|
|
137
162
|
const sourceCode = contextSourceCode(context);
|
|
138
|
-
const all = excludeShebangs(sourceCode.getAllComments(
|
|
163
|
+
const all = excludeShebangs(sourceCode.getAllComments());
|
|
164
|
+
assert.ok(all);
|
|
165
|
+
assert.ok(all.length);
|
|
139
166
|
if (all[0].type.toLowerCase() === commentTypeOptions.block) {
|
|
140
167
|
return [all[0]];
|
|
141
168
|
}
|
|
142
169
|
let i = 1;
|
|
143
170
|
for (; i < all.length; ++i) {
|
|
144
|
-
const
|
|
171
|
+
const previousRange = all[i - 1].range;
|
|
172
|
+
assertDefined(previousRange);
|
|
173
|
+
const currentRange = all[i].range;
|
|
174
|
+
assertDefined(currentRange);
|
|
175
|
+
const txt = sourceCode.text.slice(previousRange[1], currentRange[0]);
|
|
145
176
|
if (!txt.match(/^(\r\n|\r|\n)$/)) {
|
|
146
177
|
break;
|
|
147
178
|
}
|
|
@@ -152,7 +183,7 @@ function getLeadingComments(context, node) {
|
|
|
152
183
|
/**
|
|
153
184
|
* Generate a comment including trailing spaces out of a number of comment body
|
|
154
185
|
* lines.
|
|
155
|
-
* @param {
|
|
186
|
+
* @param {CommentType} commentType the type of comment to generate.
|
|
156
187
|
* @param {string[]} textArray list of lines of the comment content.
|
|
157
188
|
* @param {'\n' | '\r\n'} eol end-of-line characters.
|
|
158
189
|
* @returns {string} resulting comment.
|
|
@@ -170,12 +201,21 @@ function genCommentBody(commentType, textArray, eol) {
|
|
|
170
201
|
/**
|
|
171
202
|
* Determines the start and end position in the source code of the leading
|
|
172
203
|
* comment.
|
|
173
|
-
* @param {
|
|
204
|
+
* @param {Comment[]} comments list of comments.
|
|
174
205
|
* @returns {[number, number]} resulting range.
|
|
175
206
|
*/
|
|
176
207
|
function genCommentsRange(comments) {
|
|
177
|
-
|
|
178
|
-
const
|
|
208
|
+
assert.ok(comments.length);
|
|
209
|
+
const firstComment = comments[0];
|
|
210
|
+
assertDefined(firstComment);
|
|
211
|
+
const firstCommentRange = firstComment.range;
|
|
212
|
+
assertDefined(firstCommentRange);
|
|
213
|
+
const start = firstCommentRange[0];
|
|
214
|
+
const lastComment = comments.slice(-1)[0];
|
|
215
|
+
assertDefined(lastComment);
|
|
216
|
+
const lastCommentRange = lastComment.range;
|
|
217
|
+
assertDefined(lastCommentRange);
|
|
218
|
+
const end = lastCommentRange[1];
|
|
179
219
|
return [start, end];
|
|
180
220
|
}
|
|
181
221
|
|
|
@@ -201,7 +241,7 @@ function leadingEmptyLines(src) {
|
|
|
201
241
|
|
|
202
242
|
/**
|
|
203
243
|
* Factory for fixer that adds a missing header.
|
|
204
|
-
* @param {
|
|
244
|
+
* @param {CommentType} commentType type of comment to use.
|
|
205
245
|
* @param {RuleContext} context ESLint execution runtime.
|
|
206
246
|
* @param {string[]} headerLines lines of the header comment.
|
|
207
247
|
* @param {'\n' | '\r\n'} eol end-of-line characters
|
|
@@ -232,15 +272,13 @@ function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
|
|
|
232
272
|
|
|
233
273
|
/**
|
|
234
274
|
* Factory for fixer that replaces an incorrect header.
|
|
235
|
-
* @param {
|
|
275
|
+
* @param {CommentType} commentType type of comment to use.
|
|
236
276
|
* @param {RuleContext} context ESLint execution context.
|
|
237
277
|
* @param {Comment[]} leadingComments comment elements to replace.
|
|
238
278
|
* @param {string[]} headerLines lines of the header comment.
|
|
239
279
|
* @param {'\n' | '\r\n'} eol end-of-line characters
|
|
240
280
|
* @param {number} numNewlines number of trailing lines after the comment.
|
|
241
|
-
* @returns {
|
|
242
|
-
* (fixer: RuleTextEditor) => RuleTextEdit | RuleTextEdit[] | null
|
|
243
|
-
* } the fixer.
|
|
281
|
+
* @returns {(fixer: RuleFixer) => Fix | Fix[] | null} the fixer.
|
|
244
282
|
*/
|
|
245
283
|
function genReplaceFixer(commentType, context, leadingComments, headerLines, eol, numNewlines) {
|
|
246
284
|
return function(fixer) {
|
|
@@ -250,7 +288,7 @@ function genReplaceFixer(commentType, context, leadingComments, headerLines, eol
|
|
|
250
288
|
const eols = eol.repeat(missingNewlines);
|
|
251
289
|
return fixer.replaceTextRange(
|
|
252
290
|
commentRange,
|
|
253
|
-
genCommentBody(commentType, headerLines, eol
|
|
291
|
+
genCommentBody(commentType, headerLines, eol) + eols
|
|
254
292
|
);
|
|
255
293
|
};
|
|
256
294
|
}
|
|
@@ -261,9 +299,7 @@ function genReplaceFixer(commentType, context, leadingComments, headerLines, eol
|
|
|
261
299
|
* @param {'\n' | '\r\n'} eol end-of-line characters
|
|
262
300
|
* @param {number} missingEmptyLinesCount number of trailing lines after the
|
|
263
301
|
* comment.
|
|
264
|
-
* @returns {
|
|
265
|
-
* (fixer: RuleTextEditor) => RuleTextEdit | RuleTextEdit[] | null
|
|
266
|
-
* } the fixer.
|
|
302
|
+
* @returns {(fixer: RuleFixer) => Fix | Fix[] | null} the fixer.
|
|
267
303
|
*/
|
|
268
304
|
function genEmptyLinesFixer(leadingComments, eol, missingEmptyLinesCount) {
|
|
269
305
|
return function(fixer) {
|
|
@@ -277,7 +313,7 @@ function genEmptyLinesFixer(leadingComments, eol, missingEmptyLinesCount) {
|
|
|
277
313
|
/**
|
|
278
314
|
* Returns the used line-termination characters per the rule's config if any or
|
|
279
315
|
* else based on the runtime environments.
|
|
280
|
-
* @param {
|
|
316
|
+
* @param {LineEndingOption} style line-ending styles.
|
|
281
317
|
* @returns {'\n' | '\r\n'} the correct line ending characters for the
|
|
282
318
|
* environment.
|
|
283
319
|
*/
|
|
@@ -291,7 +327,7 @@ function getEol(style) {
|
|
|
291
327
|
return "\r\n";
|
|
292
328
|
case lineEndingOptions.os:
|
|
293
329
|
default:
|
|
294
|
-
return os.EOL;
|
|
330
|
+
return /** @type {'\n' | '\r\n'} */ (os.EOL);
|
|
295
331
|
}
|
|
296
332
|
}
|
|
297
333
|
|
|
@@ -323,8 +359,9 @@ function schemaAssert(condition, message) {
|
|
|
323
359
|
* options in that some settings are still union types and unspecified
|
|
324
360
|
* properties are not replaced by defaults. If the options follow the new
|
|
325
361
|
* format, a simple seep copy would be returned.
|
|
326
|
-
* @param {
|
|
327
|
-
*
|
|
362
|
+
* @param {AllHeaderOptions} originalOptions the options as configured by the
|
|
363
|
+
* user.
|
|
364
|
+
* @returns {HeaderOptions} the transformed new-style options with no
|
|
328
365
|
* normalization.
|
|
329
366
|
*/
|
|
330
367
|
function transformLegacyOptions(originalOptions) {
|
|
@@ -342,7 +379,7 @@ function transformLegacyOptions(originalOptions) {
|
|
|
342
379
|
// config.
|
|
343
380
|
schemaAssert(typeof originalOptions[0] === "string",
|
|
344
381
|
"first header option after severity should be either a filename or 'block' | 'line'");
|
|
345
|
-
/** @type {
|
|
382
|
+
/** @type {HeaderOptions} */
|
|
346
383
|
const transformedOptions = {};
|
|
347
384
|
// populate header
|
|
348
385
|
if (
|
|
@@ -351,7 +388,7 @@ function transformLegacyOptions(originalOptions) {
|
|
|
351
388
|
originalOptions.length === 2
|
|
352
389
|
&& typeof originalOptions[1] === "object"
|
|
353
390
|
&& !Array.isArray(originalOptions[1])
|
|
354
|
-
&& !isPattern(originalOptions[1])
|
|
391
|
+
&& !isPattern(/** @type {HeaderLine} */(originalOptions[1]))
|
|
355
392
|
)) {
|
|
356
393
|
transformedOptions.header = { file: originalOptions[0], encoding: "utf8" };
|
|
357
394
|
} else {
|
|
@@ -360,11 +397,14 @@ function transformLegacyOptions(originalOptions) {
|
|
|
360
397
|
schemaAssert(
|
|
361
398
|
typeof originalOptions[1] === "string"
|
|
362
399
|
|| Array.isArray(originalOptions[1])
|
|
363
|
-
|| isPattern(originalOptions[1]),
|
|
400
|
+
|| isPattern(/** @type {HeaderLine} */(originalOptions[1])),
|
|
364
401
|
"second header option after severity should be a string, a pattern, or an array of the previous two");
|
|
365
402
|
transformedOptions.header = {
|
|
366
|
-
commentType: originalOptions[0],
|
|
367
|
-
lines:
|
|
403
|
+
commentType: /** @type {CommentType} */ (originalOptions[0]),
|
|
404
|
+
lines: /** @type {HeaderLine[]} */ (
|
|
405
|
+
Array.isArray(originalOptions[1])
|
|
406
|
+
? originalOptions[1]
|
|
407
|
+
: [originalOptions[1]])
|
|
368
408
|
};
|
|
369
409
|
}
|
|
370
410
|
// configure required line settings
|
|
@@ -387,25 +427,42 @@ function transformLegacyOptions(originalOptions) {
|
|
|
387
427
|
return transformedOptions;
|
|
388
428
|
}
|
|
389
429
|
|
|
430
|
+
/**
|
|
431
|
+
* @param {FileBasedConfig | InlineConfig} config the header configuration.
|
|
432
|
+
* @returns {config is FileBasedConfig} true if `config` is `FileBasedConfig`.
|
|
433
|
+
*/
|
|
434
|
+
function isFileBasedHeaderConfig(config) {
|
|
435
|
+
return Object.prototype.hasOwnProperty.call(config, "file");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* @param {FileBasedConfig | InlineConfig} config the header configuration.
|
|
440
|
+
* @returns {asserts config is InlineConfig} asserts `config` is
|
|
441
|
+
* `LineBasedConfig`.
|
|
442
|
+
*/
|
|
443
|
+
function assertLineBasedHeaderConfig(config) {
|
|
444
|
+
assert.ok(Object.prototype.hasOwnProperty.call(config, "lines"));
|
|
445
|
+
}
|
|
446
|
+
|
|
390
447
|
/**
|
|
391
448
|
* Transforms a set of new-style options adding defaults and standardizing on
|
|
392
449
|
* one of multiple config styles.
|
|
393
|
-
* @param {
|
|
394
|
-
* @returns {
|
|
450
|
+
* @param {HeaderOptions} originalOptions new-style options to normalize.
|
|
451
|
+
* @returns {HeaderOptions} normalized options.
|
|
395
452
|
*/
|
|
396
453
|
function normalizeOptions(originalOptions) {
|
|
397
454
|
const options = structuredClone(originalOptions);
|
|
398
455
|
|
|
399
|
-
if (
|
|
456
|
+
if (isFileBasedHeaderConfig(originalOptions.header)) {
|
|
400
457
|
const text = fs.readFileSync(originalOptions.header.file, originalOptions.header.encoding || "utf8");
|
|
401
458
|
const [commentType, lines] = commentParser(text);
|
|
402
459
|
options.header = { commentType, lines };
|
|
403
460
|
}
|
|
404
|
-
|
|
461
|
+
assertLineBasedHeaderConfig(options.header);
|
|
405
462
|
options.header.lines = options.header.lines.flatMap(
|
|
406
463
|
(line) => {
|
|
407
464
|
if (typeof line === "string") {
|
|
408
|
-
return line.split(/\r?\n/);
|
|
465
|
+
return /** @type {HeaderLine[]} */(line.split(/\r?\n/));
|
|
409
466
|
}
|
|
410
467
|
if (line instanceof RegExp) {
|
|
411
468
|
return [{ pattern: line }];
|
|
@@ -448,7 +505,11 @@ function normalizeOptions(originalOptions) {
|
|
|
448
505
|
* @returns {SourceLocation} the location (line and column) of the violation.
|
|
449
506
|
*/
|
|
450
507
|
function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
|
|
451
|
-
|
|
508
|
+
assert.ok(leadingComments);
|
|
509
|
+
const loc = leadingComments[leadingComments.length - 1].loc;
|
|
510
|
+
assertDefined(loc);
|
|
511
|
+
assertNotNull(loc);
|
|
512
|
+
const lastCommentLineLocEnd = loc.end;
|
|
452
513
|
return {
|
|
453
514
|
start: lastCommentLineLocEnd,
|
|
454
515
|
end: {
|
|
@@ -458,18 +519,18 @@ function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
|
|
|
458
519
|
};
|
|
459
520
|
}
|
|
460
521
|
|
|
461
|
-
|
|
522
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
523
|
+
const headerRule = {
|
|
462
524
|
meta: {
|
|
463
525
|
type: "layout",
|
|
464
526
|
docs: {
|
|
465
527
|
description,
|
|
466
|
-
recommended
|
|
467
|
-
url
|
|
528
|
+
recommended
|
|
468
529
|
},
|
|
469
530
|
fixable: "whitespace",
|
|
470
531
|
schema,
|
|
471
532
|
defaultOptions: [
|
|
472
|
-
/** @type {
|
|
533
|
+
/** @type {AllHeaderOptions} */
|
|
473
534
|
{
|
|
474
535
|
lineEndings: lineEndingOptions.os,
|
|
475
536
|
trailingEmptyLines: {
|
|
@@ -497,34 +558,48 @@ module.exports = {
|
|
|
497
558
|
*/
|
|
498
559
|
create: function(context) {
|
|
499
560
|
|
|
500
|
-
const newStyleOptions = transformLegacyOptions(context.options);
|
|
561
|
+
const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */ (context.options));
|
|
501
562
|
const options = normalizeOptions(newStyleOptions);
|
|
502
563
|
|
|
503
|
-
|
|
564
|
+
assertLineBasedHeaderConfig(options.header);
|
|
565
|
+
const commentType = /** @type {CommentType} */ (options.header.commentType);
|
|
566
|
+
|
|
567
|
+
const eol = getEol(
|
|
568
|
+
/** @type {LineEndingOption} */ (options.lineEndings)
|
|
569
|
+
);
|
|
504
570
|
|
|
571
|
+
/** @type {string[]} */
|
|
505
572
|
let fixLines = [];
|
|
506
573
|
// If any of the lines are regular expressions, then we can't
|
|
507
574
|
// automatically fix them. We set this to true below once we
|
|
508
575
|
// ensure none of the lines are of type RegExp
|
|
509
576
|
let canFix = true;
|
|
510
577
|
const headerLines = options.header.lines.map(function(line) {
|
|
511
|
-
const isRegex = isPattern(line);
|
|
512
578
|
// Can only fix regex option if a template is also provided
|
|
513
|
-
if (
|
|
514
|
-
|
|
579
|
+
if (isPattern(line)) {
|
|
580
|
+
if (Object.prototype.hasOwnProperty.call(line, "template")) {
|
|
581
|
+
fixLines.push(/** @type {string} */ (line.template));
|
|
582
|
+
} else {
|
|
583
|
+
canFix = false;
|
|
584
|
+
fixLines.push("");
|
|
585
|
+
}
|
|
586
|
+
return line.pattern;
|
|
587
|
+
} else {
|
|
588
|
+
fixLines.push(/** @type {string} */ (line));
|
|
589
|
+
return line;
|
|
515
590
|
}
|
|
516
|
-
fixLines.push(line.template || line);
|
|
517
|
-
return isRegex ? line.pattern : line;
|
|
518
591
|
});
|
|
519
592
|
|
|
593
|
+
|
|
594
|
+
const numLines = /** @type {number} */ (options.trailingEmptyLines?.minimum);
|
|
595
|
+
|
|
520
596
|
return {
|
|
521
597
|
/**
|
|
522
598
|
* Hooks into the processing of the overall script node to do the
|
|
523
599
|
* header validation.
|
|
524
|
-
* @param {Program} node the whole script node
|
|
525
600
|
* @returns {void}
|
|
526
601
|
*/
|
|
527
|
-
Program: function(
|
|
602
|
+
Program: function() {
|
|
528
603
|
const sourceCode = contextSourceCode(context);
|
|
529
604
|
if (!hasHeader(sourceCode.text)) {
|
|
530
605
|
const hasShebang = sourceCode.text.startsWith("#!");
|
|
@@ -542,60 +617,72 @@ module.exports = {
|
|
|
542
617
|
},
|
|
543
618
|
messageId: "missingHeader",
|
|
544
619
|
fix: genPrependFixer(
|
|
545
|
-
|
|
620
|
+
commentType,
|
|
546
621
|
context,
|
|
547
622
|
fixLines,
|
|
548
623
|
eol,
|
|
549
|
-
|
|
624
|
+
numLines)
|
|
550
625
|
});
|
|
551
626
|
return;
|
|
552
627
|
}
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
628
|
+
const leadingComments = getLeadingComments(context);
|
|
629
|
+
const firstLeadingCommentLoc = leadingComments[0].loc;
|
|
630
|
+
const firstLeadingCommentRange = leadingComments[0].range;
|
|
631
|
+
assertDefined(firstLeadingCommentRange);
|
|
632
|
+
|
|
633
|
+
const lastLeadingCommentLoc = leadingComments[leadingComments.length - 1].loc;
|
|
634
|
+
|
|
635
|
+
if (leadingComments[0].type.toLowerCase() !== commentType) {
|
|
636
|
+
assertDefined(firstLeadingCommentLoc);
|
|
637
|
+
assertNotNull(firstLeadingCommentLoc);
|
|
638
|
+
assertDefined(lastLeadingCommentLoc);
|
|
639
|
+
assertNotNull(lastLeadingCommentLoc);
|
|
557
640
|
context.report({
|
|
558
641
|
loc: {
|
|
559
|
-
start:
|
|
560
|
-
end:
|
|
642
|
+
start: firstLeadingCommentLoc.start,
|
|
643
|
+
end: lastLeadingCommentLoc.end
|
|
561
644
|
},
|
|
562
645
|
messageId: "incorrectCommentType",
|
|
563
646
|
data: {
|
|
564
|
-
commentType:
|
|
647
|
+
commentType: commentType
|
|
565
648
|
},
|
|
566
649
|
fix: canFix
|
|
567
650
|
? genReplaceFixer(
|
|
568
|
-
|
|
651
|
+
commentType,
|
|
569
652
|
context,
|
|
570
653
|
leadingComments,
|
|
571
654
|
fixLines,
|
|
572
655
|
eol,
|
|
573
|
-
|
|
656
|
+
numLines)
|
|
574
657
|
: null
|
|
575
658
|
});
|
|
576
659
|
return;
|
|
577
660
|
}
|
|
578
|
-
if (
|
|
661
|
+
if (commentType === commentTypeOptions.line) {
|
|
579
662
|
if (headerLines.length === 1) {
|
|
580
663
|
const leadingCommentValues = leadingComments.map((c) => c.value);
|
|
581
664
|
if (
|
|
582
665
|
!match(leadingCommentValues.join("\n"), headerLines[0])
|
|
583
666
|
&& !match(leadingCommentValues.join("\r\n"), headerLines[0])
|
|
584
667
|
) {
|
|
668
|
+
assertDefined(firstLeadingCommentLoc);
|
|
669
|
+
assertNotNull(firstLeadingCommentLoc);
|
|
670
|
+
assertDefined(lastLeadingCommentLoc);
|
|
671
|
+
assertNotNull(lastLeadingCommentLoc);
|
|
585
672
|
context.report({
|
|
586
673
|
loc: {
|
|
587
|
-
start:
|
|
588
|
-
end:
|
|
674
|
+
start: firstLeadingCommentLoc.start,
|
|
675
|
+
end: lastLeadingCommentLoc.end
|
|
589
676
|
},
|
|
590
677
|
messageId: "incorrectHeader",
|
|
591
678
|
fix: canFix
|
|
592
679
|
? genReplaceFixer(
|
|
593
|
-
|
|
680
|
+
commentType,
|
|
594
681
|
context,
|
|
595
682
|
leadingComments,
|
|
596
683
|
fixLines,
|
|
597
684
|
eol,
|
|
598
|
-
|
|
685
|
+
numLines)
|
|
599
686
|
: null
|
|
600
687
|
});
|
|
601
688
|
return;
|
|
@@ -603,9 +690,12 @@ module.exports = {
|
|
|
603
690
|
} else {
|
|
604
691
|
for (let i = 0; i < headerLines.length; i++) {
|
|
605
692
|
if (leadingComments.length - 1 < i) {
|
|
693
|
+
assertDefined(lastLeadingCommentLoc);
|
|
694
|
+
assertNotNull(lastLeadingCommentLoc);
|
|
606
695
|
context.report({
|
|
607
696
|
loc: {
|
|
608
|
-
start:
|
|
697
|
+
start: lastLeadingCommentLoc.end,
|
|
698
|
+
end: lastLeadingCommentLoc.end
|
|
609
699
|
},
|
|
610
700
|
messageId: "headerTooShort",
|
|
611
701
|
data: {
|
|
@@ -613,40 +703,45 @@ module.exports = {
|
|
|
613
703
|
},
|
|
614
704
|
fix: canFix
|
|
615
705
|
? genReplaceFixer(
|
|
616
|
-
|
|
706
|
+
commentType,
|
|
617
707
|
context,
|
|
618
708
|
leadingComments,
|
|
619
709
|
fixLines,
|
|
620
710
|
eol,
|
|
621
|
-
|
|
711
|
+
numLines)
|
|
622
712
|
: null
|
|
623
713
|
});
|
|
624
714
|
return;
|
|
625
715
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
716
|
+
const headerLine = headerLines[i];
|
|
717
|
+
const comment = leadingComments[i];
|
|
718
|
+
const commentLoc = comment.loc;
|
|
719
|
+
assertDefined(commentLoc);
|
|
720
|
+
assertNotNull(commentLoc);
|
|
721
|
+
if (typeof headerLine === "string") {
|
|
722
|
+
const leadingCommentLength = comment.value.length;
|
|
723
|
+
const headerLineLength = headerLine.length;
|
|
629
724
|
for (let j = 0; j < Math.min(leadingCommentLength, headerLineLength); j++) {
|
|
630
|
-
if (
|
|
725
|
+
if (comment.value[j] !== headerLine[j]) {
|
|
631
726
|
context.report({
|
|
632
727
|
loc: {
|
|
633
728
|
start: {
|
|
634
729
|
column: "//".length + j,
|
|
635
|
-
line:
|
|
730
|
+
line: commentLoc.start.line
|
|
636
731
|
},
|
|
637
|
-
end:
|
|
732
|
+
end: commentLoc.end
|
|
638
733
|
},
|
|
639
734
|
messageId: "headerLineMismatchAtPos",
|
|
640
735
|
data: {
|
|
641
|
-
expected:
|
|
736
|
+
expected: headerLine.substring(j)
|
|
642
737
|
},
|
|
643
738
|
fix: genReplaceFixer(
|
|
644
|
-
|
|
739
|
+
commentType,
|
|
645
740
|
context,
|
|
646
741
|
leadingComments,
|
|
647
742
|
fixLines,
|
|
648
743
|
eol,
|
|
649
|
-
|
|
744
|
+
numLines)
|
|
650
745
|
});
|
|
651
746
|
return;
|
|
652
747
|
}
|
|
@@ -654,20 +749,21 @@ module.exports = {
|
|
|
654
749
|
if (leadingCommentLength < headerLineLength) {
|
|
655
750
|
context.report({
|
|
656
751
|
loc: {
|
|
657
|
-
start:
|
|
752
|
+
start: commentLoc.end,
|
|
753
|
+
end: commentLoc.end,
|
|
658
754
|
},
|
|
659
755
|
messageId: "headerLineTooShort",
|
|
660
756
|
data: {
|
|
661
|
-
remainder:
|
|
757
|
+
remainder: headerLine.substring(leadingCommentLength)
|
|
662
758
|
},
|
|
663
759
|
fix: canFix
|
|
664
760
|
? genReplaceFixer(
|
|
665
|
-
|
|
761
|
+
commentType,
|
|
666
762
|
context,
|
|
667
763
|
leadingComments,
|
|
668
764
|
fixLines,
|
|
669
765
|
eol,
|
|
670
|
-
|
|
766
|
+
numLines)
|
|
671
767
|
: null
|
|
672
768
|
});
|
|
673
769
|
return;
|
|
@@ -677,45 +773,45 @@ module.exports = {
|
|
|
677
773
|
loc: {
|
|
678
774
|
start: {
|
|
679
775
|
column: "//".length + headerLineLength,
|
|
680
|
-
line:
|
|
776
|
+
line: commentLoc.start.line
|
|
681
777
|
},
|
|
682
|
-
end:
|
|
778
|
+
end: commentLoc.end,
|
|
683
779
|
},
|
|
684
780
|
messageId: "headerLineTooLong",
|
|
685
781
|
fix: canFix
|
|
686
782
|
? genReplaceFixer(
|
|
687
|
-
|
|
783
|
+
commentType,
|
|
688
784
|
context,
|
|
689
785
|
leadingComments,
|
|
690
786
|
fixLines,
|
|
691
787
|
eol,
|
|
692
|
-
|
|
788
|
+
numLines)
|
|
693
789
|
: null
|
|
694
790
|
});
|
|
695
791
|
return;
|
|
696
792
|
}
|
|
697
793
|
} else {
|
|
698
|
-
if (!match(
|
|
794
|
+
if (!match(comment.value, headerLine)) {
|
|
699
795
|
context.report({
|
|
700
796
|
loc: {
|
|
701
797
|
start: {
|
|
702
798
|
column: "//".length,
|
|
703
|
-
line:
|
|
799
|
+
line: commentLoc.start.line,
|
|
704
800
|
},
|
|
705
|
-
end:
|
|
801
|
+
end: commentLoc.end,
|
|
706
802
|
},
|
|
707
803
|
messageId: "incorrectHeaderLine",
|
|
708
804
|
data: {
|
|
709
|
-
pattern:
|
|
805
|
+
pattern: headerLine.toString()
|
|
710
806
|
},
|
|
711
807
|
fix: canFix
|
|
712
808
|
? genReplaceFixer(
|
|
713
|
-
|
|
809
|
+
commentType,
|
|
714
810
|
context,
|
|
715
811
|
leadingComments,
|
|
716
812
|
fixLines,
|
|
717
813
|
eol,
|
|
718
|
-
|
|
814
|
+
numLines)
|
|
719
815
|
: null
|
|
720
816
|
});
|
|
721
817
|
return;
|
|
@@ -724,15 +820,16 @@ module.exports = {
|
|
|
724
820
|
}
|
|
725
821
|
}
|
|
726
822
|
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
const
|
|
823
|
+
const commentRange = leadingComments[headerLines.length - 1].range;
|
|
824
|
+
assertDefined(commentRange);
|
|
825
|
+
const actualLeadingEmptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
|
|
826
|
+
const missingEmptyLines = numLines - actualLeadingEmptyLines;
|
|
730
827
|
if (missingEmptyLines > 0) {
|
|
731
828
|
context.report({
|
|
732
829
|
loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
|
|
733
830
|
messageId: "noNewlineAfterHeader",
|
|
734
831
|
data: {
|
|
735
|
-
expected:
|
|
832
|
+
expected: numLines,
|
|
736
833
|
actual: actualLeadingEmptyLines
|
|
737
834
|
},
|
|
738
835
|
fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
|
|
@@ -749,8 +846,8 @@ module.exports = {
|
|
|
749
846
|
|
|
750
847
|
/** @type {null | string} */
|
|
751
848
|
let errorMessageId = null;
|
|
752
|
-
/** @type {
|
|
753
|
-
let errorMessageData
|
|
849
|
+
/** @type {undefined | Record<string, string>} */
|
|
850
|
+
let errorMessageData;
|
|
754
851
|
/** @type {null | SourceLocation} */
|
|
755
852
|
let errorMessageLoc = null;
|
|
756
853
|
for (let i = 0; i < headerLines.length; i++) {
|
|
@@ -761,7 +858,9 @@ module.exports = {
|
|
|
761
858
|
if (leadingLine[j] !== headerLine[j]) {
|
|
762
859
|
errorMessageId = "headerLineMismatchAtPos";
|
|
763
860
|
const columnOffset = i === 0 ? "/*".length : 0;
|
|
764
|
-
|
|
861
|
+
assertDefined(firstLeadingCommentLoc);
|
|
862
|
+
assertNotNull(firstLeadingCommentLoc);
|
|
863
|
+
const line = firstLeadingCommentLoc.start.line + i;
|
|
765
864
|
errorMessageLoc = {
|
|
766
865
|
start: {
|
|
767
866
|
column: columnOffset + j,
|
|
@@ -784,31 +883,35 @@ module.exports = {
|
|
|
784
883
|
if (leadingLine.length < headerLine.length) {
|
|
785
884
|
errorMessageId = "headerLineTooShort";
|
|
786
885
|
const startColumn = (i === 0 ? "/*".length : 0) + leadingLine.length;
|
|
886
|
+
assertDefined(firstLeadingCommentLoc);
|
|
887
|
+
assertNotNull(firstLeadingCommentLoc);
|
|
787
888
|
errorMessageLoc = {
|
|
788
889
|
start: {
|
|
789
890
|
column: startColumn,
|
|
790
|
-
line:
|
|
891
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
791
892
|
},
|
|
792
893
|
end: {
|
|
793
894
|
column: startColumn + 1,
|
|
794
|
-
line:
|
|
895
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
795
896
|
}
|
|
796
897
|
};
|
|
797
898
|
errorMessageData = {
|
|
798
|
-
remainder:
|
|
899
|
+
remainder: headerLine.substring(leadingLine.length)
|
|
799
900
|
};
|
|
800
901
|
break;
|
|
801
902
|
}
|
|
802
903
|
if (leadingLine.length > headerLine.length) {
|
|
904
|
+
assertDefined(firstLeadingCommentLoc);
|
|
905
|
+
assertNotNull(firstLeadingCommentLoc);
|
|
803
906
|
errorMessageId = "headerLineTooLong";
|
|
804
907
|
errorMessageLoc = {
|
|
805
908
|
start: {
|
|
806
909
|
column: (i === 0 ? "/*".length : 0) + headerLine.length,
|
|
807
|
-
line:
|
|
910
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
808
911
|
},
|
|
809
912
|
end: {
|
|
810
913
|
column: (i === 0 ? "/*".length : 0) + leadingLine.length,
|
|
811
|
-
line:
|
|
914
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
812
915
|
}
|
|
813
916
|
};
|
|
814
917
|
break;
|
|
@@ -817,17 +920,19 @@ module.exports = {
|
|
|
817
920
|
if (!match(leadingLine, headerLine)) {
|
|
818
921
|
errorMessageId = "incorrectHeaderLine";
|
|
819
922
|
errorMessageData = {
|
|
820
|
-
pattern: headerLine
|
|
923
|
+
pattern: headerLine.toString()
|
|
821
924
|
};
|
|
822
925
|
const columnOffset = i === 0 ? "/*".length : 0;
|
|
926
|
+
assertDefined(firstLeadingCommentLoc);
|
|
927
|
+
assertNotNull(firstLeadingCommentLoc);
|
|
823
928
|
errorMessageLoc = {
|
|
824
929
|
start: {
|
|
825
930
|
column: columnOffset + 0,
|
|
826
|
-
line:
|
|
931
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
827
932
|
},
|
|
828
933
|
end: {
|
|
829
934
|
column: columnOffset + leadingLine.length,
|
|
830
|
-
line:
|
|
935
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
831
936
|
}
|
|
832
937
|
};
|
|
833
938
|
break;
|
|
@@ -837,14 +942,18 @@ module.exports = {
|
|
|
837
942
|
|
|
838
943
|
if (!errorMessageId && leadingLines.length > headerLines.length) {
|
|
839
944
|
errorMessageId = "headerTooLong";
|
|
945
|
+
assertDefined(firstLeadingCommentLoc);
|
|
946
|
+
assertNotNull(firstLeadingCommentLoc);
|
|
947
|
+
assertDefined(lastLeadingCommentLoc);
|
|
948
|
+
assertNotNull(lastLeadingCommentLoc);
|
|
840
949
|
errorMessageLoc = {
|
|
841
950
|
start: {
|
|
842
951
|
column: (headerLines.length === 0 ? "/*".length : 0) + 0,
|
|
843
|
-
line:
|
|
952
|
+
line: firstLeadingCommentLoc.start.line + headerLines.length
|
|
844
953
|
},
|
|
845
954
|
end: {
|
|
846
|
-
column:
|
|
847
|
-
line:
|
|
955
|
+
column: lastLeadingCommentLoc.end.column - "*/".length,
|
|
956
|
+
line: lastLeadingCommentLoc.end.line
|
|
848
957
|
}
|
|
849
958
|
};
|
|
850
959
|
}
|
|
@@ -853,32 +962,33 @@ module.exports = {
|
|
|
853
962
|
if (canFix && headerLines.length > 1) {
|
|
854
963
|
fixLines = [fixLines.join(eol)];
|
|
855
964
|
}
|
|
965
|
+
assertNotNull(errorMessageLoc);
|
|
856
966
|
context.report({
|
|
857
967
|
loc: errorMessageLoc,
|
|
858
968
|
messageId: errorMessageId,
|
|
859
969
|
data: errorMessageData,
|
|
860
970
|
fix: canFix
|
|
861
971
|
? genReplaceFixer(
|
|
862
|
-
|
|
972
|
+
commentType,
|
|
863
973
|
context,
|
|
864
974
|
leadingComments,
|
|
865
975
|
fixLines,
|
|
866
976
|
eol,
|
|
867
|
-
|
|
977
|
+
numLines)
|
|
868
978
|
: null
|
|
869
979
|
});
|
|
870
980
|
return;
|
|
871
981
|
}
|
|
872
982
|
|
|
873
983
|
const actualLeadingEmptyLines =
|
|
874
|
-
leadingEmptyLines(sourceCode.text.substring(
|
|
875
|
-
const missingEmptyLines =
|
|
984
|
+
leadingEmptyLines(sourceCode.text.substring(firstLeadingCommentRange[1]));
|
|
985
|
+
const missingEmptyLines = numLines - actualLeadingEmptyLines;
|
|
876
986
|
if (missingEmptyLines > 0) {
|
|
877
987
|
context.report({
|
|
878
988
|
loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
|
|
879
989
|
messageId: "noNewlineAfterHeader",
|
|
880
990
|
data: {
|
|
881
|
-
expected:
|
|
991
|
+
expected: numLines,
|
|
882
992
|
actual: actualLeadingEmptyLines
|
|
883
993
|
},
|
|
884
994
|
fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
|
|
@@ -889,3 +999,4 @@ module.exports = {
|
|
|
889
999
|
}
|
|
890
1000
|
};
|
|
891
1001
|
|
|
1002
|
+
exports.header = headerRule;
|