@tony.ganchev/eslint-plugin-header 3.2.4 → 3.2.6
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/CONTRIBUTING.md +8 -8
- package/LICENSE.md +2 -1
- package/index.d.ts +5 -5
- package/index.js +6 -6
- package/lib/comment-parser.js +9 -8
- package/lib/rules/eslint-utils.js +9 -9
- package/lib/rules/header.docs.js +5 -6
- package/lib/rules/header.js +543 -572
- package/lib/rules/header.schema.js +7 -6
- package/package.json +26 -26
- package/types/index.d.ts +1 -1
- package/types/lib/comment-parser.d.ts +1 -1
- package/types/lib/comment-parser.d.ts.map +1 -1
- package/types/lib/rules/header.d.ts +88 -53
- package/types/lib/rules/header.d.ts.map +1 -1
- package/types/lib/rules/header.schema.d.ts +1 -1
- package/types/lib/rules/header.schema.d.ts.map +1 -1
package/lib/rules/header.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Copyright (c)
|
|
5
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* @file Header validation rule implementation.
|
|
3
|
+
* @copyright Copyright (c) 2015-present Stuart Knightley and contributors
|
|
4
|
+
* @copyright Copyright (c) 2024-2026 Tony Ganchev
|
|
5
|
+
* @license MIT
|
|
6
6
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
* of this software and associated documentation files (the “Software”), to deal
|
|
8
8
|
* in the Software without restriction, including without limitation the rights
|
|
@@ -33,66 +33,118 @@ const { description, recommended } = require("./header.docs");
|
|
|
33
33
|
const { lineEndingOptions, commentTypeOptions, schema } = require("./header.schema");
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
* @
|
|
38
|
-
* @
|
|
39
|
-
* @typedef {
|
|
40
|
-
* @typedef {
|
|
41
|
-
* @typedef {
|
|
42
|
-
* @typedef {
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
* @import { JSSyntaxElement, Linter, Rule, SourceCode } from "eslint"
|
|
37
|
+
* @import { Comment, SourceLocation } from "estree"
|
|
38
|
+
* @import { ViolationReport } from "@eslint/core";
|
|
39
|
+
* @typedef {Rule.NodeListener} NodeListener
|
|
40
|
+
* @typedef {Rule.ReportFixer} ReportFixer
|
|
41
|
+
* @typedef {Rule.RuleFixer} RuleFixer
|
|
42
|
+
* @typedef {Rule.RuleContext} RuleContext
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {"\n" | "\r\n"} LineEnding The sequence of characters that define
|
|
47
|
+
* the end of a line.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {object} HeaderLinePattern Matching rule for a line from the header
|
|
52
|
+
* using regular expression and optionally providing an auto-fix replacement.
|
|
53
|
+
* @property {string | RegExp} pattern A regular expression that should match
|
|
54
|
+
* the header line.
|
|
55
|
+
* @property {string} [template] When set, if the actual header line does not
|
|
56
|
+
* match `pattern`, this value is to be used when running auto-fix.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {string | RegExp | HeaderLinePattern} HeaderLine Matching rule for
|
|
61
|
+
* a single line of the header comment or the header comment as a whole if only
|
|
62
|
+
* one used.
|
|
63
|
+
* @typedef {HeaderLine | HeaderLine[]} HeaderLines The set of header comment-
|
|
64
|
+
* matching rules.
|
|
65
|
+
* @typedef {"os" | "unix" | "windows"} LineEndingOption Defines what EOL
|
|
66
|
+
* characters to expect - either forced to be Windows / POSIX-compatible, or
|
|
67
|
+
* defaulting to whatever the OS expects.
|
|
68
|
+
* @typedef {{ lineEndings?: LineEndingOption }} HeaderSettings How to treat
|
|
69
|
+
* line endings.
|
|
70
|
+
* @typedef {"block" | "line"} CommentType The expected type of comment to use
|
|
71
|
+
* for the header.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {object} FileBasedConfig Header content configuration defined in a
|
|
76
|
+
* separate JavaScript template file.
|
|
77
|
+
* @property {string} file Template file path relative to the directory
|
|
78
|
+
* from which ESLint runs.
|
|
79
|
+
* @property {BufferEncoding} [encoding] Encoding to use when reading the
|
|
80
|
+
* file. If omitted, `"utf8"` will be assumed.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {object} InlineConfig Header content configuration defined inline
|
|
85
|
+
* within the ESLint configuration.
|
|
86
|
+
* @property {CommentType} commentType The type of comment to expect.
|
|
87
|
+
* @property {HeaderLine[]} lines Matching rules for lines. If only one rule
|
|
88
|
+
* is provided, the rule would attempt to match either the first line ar all
|
|
89
|
+
* lines together.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {object} TrailingEmptyLines Rule configuration on the handling of
|
|
94
|
+
* empty lines after the header comment.
|
|
95
|
+
* @property {number} [minimum] If set, the rule would check that at least
|
|
96
|
+
* a `minimum` number of EOL characters trail the header.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @typedef {object} HeaderOptionsWithoutSettings
|
|
101
|
+
* @property {FileBasedConfig | InlineConfig} header The text matching rules
|
|
102
|
+
* for the header.
|
|
103
|
+
* @property {TrailingEmptyLines} [trailingEmptyLines] Rules about empty lines
|
|
104
|
+
* after the header comment.
|
|
45
105
|
*/
|
|
46
106
|
|
|
47
107
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* @typedef {
|
|
54
|
-
* @typedef {
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* lines: HeaderLines,
|
|
81
|
-
* minLines: number,
|
|
82
|
-
* settings: HeaderSettings
|
|
83
|
-
* ]
|
|
84
|
-
* } AllHeaderOptions
|
|
85
|
-
* @typedef {import('eslint').Linter.RuleEntry<AllHeaderOptions>
|
|
86
|
-
* } HeaderRuleConfig
|
|
108
|
+
* @typedef {HeaderOptionsWithoutSettings & HeaderSettings} HeaderOptions Modern
|
|
109
|
+
* object-based rule configuration.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @typedef {[template: string]} LegacyFileBasedConfig
|
|
114
|
+
* @typedef {[template: string, settings: HeaderSettings]
|
|
115
|
+
* } LegacyFileBasedSettingsConfig
|
|
116
|
+
* @typedef {[type: CommentType, lines: HeaderLines]} LegacyInlineConfig
|
|
117
|
+
* @typedef {[type: CommentType, lines: HeaderLines, settings: HeaderSettings]
|
|
118
|
+
* } LegacyInlineSettingsConfig
|
|
119
|
+
* @typedef {[type: CommentType, lines: HeaderLines, minLines: number]
|
|
120
|
+
* } LegacyInlineMinLinesConfig
|
|
121
|
+
* @typedef {[
|
|
122
|
+
* type: CommentType,
|
|
123
|
+
* lines: HeaderLines,
|
|
124
|
+
* minLines: number,
|
|
125
|
+
* settings: HeaderSettings
|
|
126
|
+
* ]} LegacyInlineMinLinesSettingsConfig
|
|
127
|
+
* @typedef {[HeaderOptions]
|
|
128
|
+
* | LegacyFileBasedConfig
|
|
129
|
+
* | LegacyFileBasedSettingsConfig
|
|
130
|
+
* | LegacyInlineConfig
|
|
131
|
+
* | LegacyInlineSettingsConfig
|
|
132
|
+
* | LegacyInlineMinLinesConfig
|
|
133
|
+
* | LegacyInlineMinLinesSettingsConfig
|
|
134
|
+
* } AllHeaderOptions Full possible rule configuration options.
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @typedef {Linter.RuleEntry<AllHeaderOptions>} HeaderRuleConfig Rule
|
|
139
|
+
* configuration array including severity.
|
|
87
140
|
*/
|
|
88
141
|
|
|
89
142
|
/**
|
|
90
143
|
* Tests if the passed line configuration string or object is a pattern
|
|
91
144
|
* definition.
|
|
92
|
-
* @param {HeaderLine} object
|
|
145
|
+
* @param {HeaderLine} object Line configuration object or string.
|
|
93
146
|
* @returns {object is HeaderLinePattern} `true` if the line configuration is a
|
|
94
|
-
*
|
|
95
|
-
* otherwise.
|
|
147
|
+
* pattern-defining object or `false` otherwise.
|
|
96
148
|
*/
|
|
97
149
|
function isPattern(object) {
|
|
98
150
|
return typeof object === "object"
|
|
@@ -102,10 +154,10 @@ function isPattern(object) {
|
|
|
102
154
|
/**
|
|
103
155
|
* Utility over a line config argument to match an expected string either
|
|
104
156
|
* against a regex or for full match against a string.
|
|
105
|
-
* @param {string} actual
|
|
157
|
+
* @param {string} actual The string to test.
|
|
106
158
|
* @param {string | RegExp} expected The string or regex to test again.
|
|
107
159
|
* @returns {boolean} `true` if the passed string matches the expected line
|
|
108
|
-
*
|
|
160
|
+
* config or `false` otherwise.
|
|
109
161
|
*/
|
|
110
162
|
function match(actual, expected) {
|
|
111
163
|
if (expected instanceof RegExp) {
|
|
@@ -117,61 +169,35 @@ function match(actual, expected) {
|
|
|
117
169
|
|
|
118
170
|
/**
|
|
119
171
|
* Remove Unix she-bangs from the list of comments.
|
|
120
|
-
* @param {(Comment | { type: "Shebang" })[]} comments
|
|
121
|
-
*
|
|
122
|
-
* @returns {Comment[]}
|
|
123
|
-
*
|
|
124
|
-
* omitted.
|
|
172
|
+
* @param {(Comment | { type: "Shebang" })[]} comments The list of comment
|
|
173
|
+
* lines.
|
|
174
|
+
* @returns {Comment[]} The list of comments with containing all incoming
|
|
175
|
+
* comments from `comments` with the shebang comments omitted.
|
|
125
176
|
*/
|
|
126
177
|
function excludeShebangs(comments) {
|
|
127
178
|
/** @type {Comment[]} */
|
|
128
|
-
return comments.filter(function(comment) {
|
|
179
|
+
return comments.filter(function (comment) {
|
|
129
180
|
return comment.type !== "Shebang";
|
|
130
181
|
});
|
|
131
182
|
}
|
|
132
183
|
|
|
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
|
-
|
|
153
184
|
/**
|
|
154
185
|
* Returns either the first block comment or the first set of line comments that
|
|
155
186
|
* are ONLY separated by a single newline. Note that this does not actually
|
|
156
187
|
* check if they are at the start of the file since that is already checked by
|
|
157
188
|
* `hasHeader()`.
|
|
158
|
-
* @param {
|
|
159
|
-
* @returns {Comment[]}
|
|
189
|
+
* @param {SourceCode} sourceCode AST.
|
|
190
|
+
* @returns {Comment[]} Lines That constitute the leading comment.
|
|
160
191
|
*/
|
|
161
|
-
function getLeadingComments(
|
|
162
|
-
const sourceCode = contextSourceCode(context);
|
|
192
|
+
function getLeadingComments(sourceCode) {
|
|
163
193
|
const all = excludeShebangs(sourceCode.getAllComments());
|
|
164
|
-
assert.ok(all);
|
|
165
|
-
assert.ok(all.length);
|
|
166
194
|
if (all[0].type.toLowerCase() === commentTypeOptions.block) {
|
|
167
195
|
return [all[0]];
|
|
168
196
|
}
|
|
169
197
|
let i = 1;
|
|
170
198
|
for (; i < all.length; ++i) {
|
|
171
|
-
const previousRange = all[i - 1].range;
|
|
172
|
-
|
|
173
|
-
const currentRange = all[i].range;
|
|
174
|
-
assertDefined(currentRange);
|
|
199
|
+
const previousRange = /** @type {[number, number]} */ (all[i - 1].range);
|
|
200
|
+
const currentRange = /** @type {[number, number]} */ (all[i].range);
|
|
175
201
|
const txt = sourceCode.text.slice(previousRange[1], currentRange[0]);
|
|
176
202
|
if (!txt.match(/^(\r\n|\r|\n)$/)) {
|
|
177
203
|
break;
|
|
@@ -183,10 +209,10 @@ function getLeadingComments(context) {
|
|
|
183
209
|
/**
|
|
184
210
|
* Generate a comment including trailing spaces out of a number of comment body
|
|
185
211
|
* lines.
|
|
186
|
-
* @param {CommentType} commentType
|
|
187
|
-
* @param {string[]} textArray
|
|
188
|
-
* @param {
|
|
189
|
-
* @returns {string}
|
|
212
|
+
* @param {CommentType} commentType The type of comment to generate.
|
|
213
|
+
* @param {string[]} textArray List of lines of the comment content.
|
|
214
|
+
* @param {LineEnding} eol End-of-line characters.
|
|
215
|
+
* @returns {string} Resulting comment.
|
|
190
216
|
*/
|
|
191
217
|
function genCommentBody(commentType, textArray, eol) {
|
|
192
218
|
if (commentType === commentTypeOptions.block) {
|
|
@@ -201,20 +227,16 @@ function genCommentBody(commentType, textArray, eol) {
|
|
|
201
227
|
/**
|
|
202
228
|
* Determines the start and end position in the source code of the leading
|
|
203
229
|
* comment.
|
|
204
|
-
* @param {Comment[]} comments
|
|
205
|
-
* @returns {[number, number]}
|
|
230
|
+
* @param {Comment[]} comments List of comments.
|
|
231
|
+
* @returns {[number, number]} Resulting range.
|
|
206
232
|
*/
|
|
207
233
|
function genCommentsRange(comments) {
|
|
208
234
|
assert.ok(comments.length);
|
|
209
235
|
const firstComment = comments[0];
|
|
210
|
-
|
|
211
|
-
const firstCommentRange = firstComment.range;
|
|
212
|
-
assertDefined(firstCommentRange);
|
|
236
|
+
const firstCommentRange = /** @type {[number, number]} */ (firstComment.range);
|
|
213
237
|
const start = firstCommentRange[0];
|
|
214
238
|
const lastComment = comments.slice(-1)[0];
|
|
215
|
-
|
|
216
|
-
const lastCommentRange = lastComment.range;
|
|
217
|
-
assertDefined(lastCommentRange);
|
|
239
|
+
const lastCommentRange = /** @type {[number, number]} */ (lastComment.range);
|
|
218
240
|
const end = lastCommentRange[1];
|
|
219
241
|
return [start, end];
|
|
220
242
|
}
|
|
@@ -222,8 +244,8 @@ function genCommentsRange(comments) {
|
|
|
222
244
|
/**
|
|
223
245
|
* Calculates the number of leading empty lines in the source code. The function
|
|
224
246
|
* counts both Windows and POSIX line endings.
|
|
225
|
-
* @param {string} src
|
|
226
|
-
* @returns {number}
|
|
247
|
+
* @param {string} src The source code to traverse.
|
|
248
|
+
* @returns {number} The number of leading empty lines.
|
|
227
249
|
*/
|
|
228
250
|
function leadingEmptyLines(src) {
|
|
229
251
|
let numLines = 0;
|
|
@@ -241,18 +263,17 @@ function leadingEmptyLines(src) {
|
|
|
241
263
|
|
|
242
264
|
/**
|
|
243
265
|
* Factory for fixer that adds a missing header.
|
|
244
|
-
* @param {CommentType} commentType
|
|
245
|
-
* @param {
|
|
246
|
-
* @param {string[]} headerLines
|
|
247
|
-
* @param {
|
|
248
|
-
* @param {number} numNewlines
|
|
249
|
-
* @returns {ReportFixer}
|
|
266
|
+
* @param {CommentType} commentType Type of comment to use.
|
|
267
|
+
* @param {SourceCode} sourceCode AST.
|
|
268
|
+
* @param {string[]} headerLines Lines of the header comment.
|
|
269
|
+
* @param {LineEnding} eol End-of-line characters.
|
|
270
|
+
* @param {number} numNewlines Number of trailing lines after the comment.
|
|
271
|
+
* @returns {ReportFixer} The fix to apply.
|
|
250
272
|
*/
|
|
251
|
-
function genPrependFixer(commentType,
|
|
252
|
-
return function(fixer) {
|
|
273
|
+
function genPrependFixer(commentType, sourceCode, headerLines, eol, numNewlines) {
|
|
274
|
+
return function (fixer) {
|
|
253
275
|
let insertPos = 0;
|
|
254
276
|
let newHeader = genCommentBody(commentType, headerLines, eol);
|
|
255
|
-
const sourceCode = contextSourceCode(context);
|
|
256
277
|
if (sourceCode.text.startsWith("#!")) {
|
|
257
278
|
const firstNewLinePos = sourceCode.text.indexOf("\n");
|
|
258
279
|
insertPos = firstNewLinePos === -1 ? sourceCode.text.length : firstNewLinePos + 1;
|
|
@@ -272,18 +293,18 @@ function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
|
|
|
272
293
|
|
|
273
294
|
/**
|
|
274
295
|
* Factory for fixer that replaces an incorrect header.
|
|
275
|
-
* @param {CommentType} commentType
|
|
276
|
-
* @param {
|
|
277
|
-
* @param {Comment[]} leadingComments
|
|
278
|
-
* @param {string[]} headerLines
|
|
279
|
-
* @param {
|
|
280
|
-
* @param {number} numNewlines
|
|
281
|
-
* @returns {
|
|
296
|
+
* @param {CommentType} commentType Type of comment to use.
|
|
297
|
+
* @param {SourceCode} sourceCode AST.
|
|
298
|
+
* @param {Comment[]} leadingComments Comment elements to replace.
|
|
299
|
+
* @param {string[]} headerLines Lines of the header comment.
|
|
300
|
+
* @param {LineEnding} eol End-of-line characters.
|
|
301
|
+
* @param {number} numNewlines Number of trailing lines after the comment.
|
|
302
|
+
* @returns {ReportFixer} The fix to apply.
|
|
282
303
|
*/
|
|
283
|
-
function genReplaceFixer(commentType,
|
|
284
|
-
return function(fixer) {
|
|
304
|
+
function genReplaceFixer(commentType, sourceCode, leadingComments, headerLines, eol, numNewlines) {
|
|
305
|
+
return function (fixer) {
|
|
285
306
|
const commentRange = genCommentsRange(leadingComments);
|
|
286
|
-
const emptyLines = leadingEmptyLines(
|
|
307
|
+
const emptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
|
|
287
308
|
const missingNewlines = Math.max(0, numNewlines - emptyLines);
|
|
288
309
|
const eols = eol.repeat(missingNewlines);
|
|
289
310
|
return fixer.replaceTextRange(
|
|
@@ -295,14 +316,14 @@ function genReplaceFixer(commentType, context, leadingComments, headerLines, eol
|
|
|
295
316
|
|
|
296
317
|
/**
|
|
297
318
|
* Factory for fixer that replaces an incorrect header.
|
|
298
|
-
* @param {Comment[]} leadingComments
|
|
299
|
-
* @param {
|
|
300
|
-
* @param {number} missingEmptyLinesCount
|
|
301
|
-
*
|
|
302
|
-
* @returns {
|
|
319
|
+
* @param {Comment[]} leadingComments Comment elements to replace.
|
|
320
|
+
* @param {LineEnding} eol End-of-line characters.
|
|
321
|
+
* @param {number} missingEmptyLinesCount Number of trailing lines after the
|
|
322
|
+
* comment.
|
|
323
|
+
* @returns {ReportFixer} The fix to apply.
|
|
303
324
|
*/
|
|
304
325
|
function genEmptyLinesFixer(leadingComments, eol, missingEmptyLinesCount) {
|
|
305
|
-
return function(fixer) {
|
|
326
|
+
return function (fixer) {
|
|
306
327
|
return fixer.insertTextAfterRange(
|
|
307
328
|
genCommentsRange(leadingComments),
|
|
308
329
|
eol.repeat(missingEmptyLinesCount)
|
|
@@ -313,9 +334,8 @@ function genEmptyLinesFixer(leadingComments, eol, missingEmptyLinesCount) {
|
|
|
313
334
|
/**
|
|
314
335
|
* Returns the used line-termination characters per the rule's config if any or
|
|
315
336
|
* else based on the runtime environments.
|
|
316
|
-
* @param {LineEndingOption} style
|
|
317
|
-
* @returns {
|
|
318
|
-
* environment.
|
|
337
|
+
* @param {LineEndingOption} style Line-ending styles.
|
|
338
|
+
* @returns {LineEnding} The correct line ending characters for the environment.
|
|
319
339
|
*/
|
|
320
340
|
function getEol(style) {
|
|
321
341
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(lineEndingOptions, style), true,
|
|
@@ -327,14 +347,14 @@ function getEol(style) {
|
|
|
327
347
|
return "\r\n";
|
|
328
348
|
case lineEndingOptions.os:
|
|
329
349
|
default:
|
|
330
|
-
return /** @type {
|
|
350
|
+
return /** @type {LineEnding} */ (os.EOL);
|
|
331
351
|
}
|
|
332
352
|
}
|
|
333
353
|
|
|
334
354
|
/**
|
|
335
355
|
* Tests if the first line in the source code (after a Unix she-bang) is a
|
|
336
356
|
* comment. Does not tolerate empty lines before the first match.
|
|
337
|
-
* @param {string} src
|
|
357
|
+
* @param {string} src Source code to test.
|
|
338
358
|
* @returns {boolean} `true` if there is a comment or `false` otherwise.
|
|
339
359
|
*/
|
|
340
360
|
function hasHeader(src) {
|
|
@@ -343,10 +363,10 @@ function hasHeader(src) {
|
|
|
343
363
|
}
|
|
344
364
|
|
|
345
365
|
/**
|
|
346
|
-
*
|
|
366
|
+
* Asserts on an expression and adds template texts to the failure message.
|
|
347
367
|
* Helper to write cleaner code.
|
|
348
|
-
* @param {boolean} condition
|
|
349
|
-
* @param {string} message
|
|
368
|
+
* @param {boolean} condition Condition to verify.
|
|
369
|
+
* @param {string} message Assert message on violation.
|
|
350
370
|
*/
|
|
351
371
|
function schemaAssert(condition, message) {
|
|
352
372
|
assert.strictEqual(condition, true, message + " - should have been handled by eslint schema validation.");
|
|
@@ -359,10 +379,10 @@ function schemaAssert(condition, message) {
|
|
|
359
379
|
* options in that some settings are still union types and unspecified
|
|
360
380
|
* properties are not replaced by defaults. If the options follow the new
|
|
361
381
|
* format, a simple seep copy would be returned.
|
|
362
|
-
* @param {AllHeaderOptions} originalOptions
|
|
363
|
-
*
|
|
364
|
-
* @returns {HeaderOptions}
|
|
365
|
-
*
|
|
382
|
+
* @param {AllHeaderOptions} originalOptions The options as configured by the
|
|
383
|
+
* user.
|
|
384
|
+
* @returns {HeaderOptions} The transformed new-style options with no
|
|
385
|
+
* normalization.
|
|
366
386
|
*/
|
|
367
387
|
function transformLegacyOptions(originalOptions) {
|
|
368
388
|
schemaAssert(originalOptions?.length > 0,
|
|
@@ -428,38 +448,29 @@ function transformLegacyOptions(originalOptions) {
|
|
|
428
448
|
}
|
|
429
449
|
|
|
430
450
|
/**
|
|
431
|
-
*
|
|
432
|
-
* @
|
|
451
|
+
* Type guard for `FileBasedConfig`.
|
|
452
|
+
* @param {FileBasedConfig | InlineConfig} config The header configuration.
|
|
453
|
+
* @returns {config is FileBasedConfig} `true` if `config` is `FileBasedConfig`,
|
|
454
|
+
* else `false`.
|
|
433
455
|
*/
|
|
434
456
|
function isFileBasedHeaderConfig(config) {
|
|
435
457
|
return Object.prototype.hasOwnProperty.call(config, "file");
|
|
436
458
|
}
|
|
437
459
|
|
|
438
460
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
461
|
+
* Transforms file template-based matching rules to inline rules for further
|
|
462
|
+
* use.
|
|
463
|
+
* @param {FileBasedConfig | InlineConfig} matcher The matching rule.
|
|
464
|
+
* @returns {InlineConfig} The resulting normalized configuration.
|
|
442
465
|
*/
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Transforms a set of new-style options adding defaults and standardizing on
|
|
449
|
-
* one of multiple config styles.
|
|
450
|
-
* @param {HeaderOptions} originalOptions new-style options to normalize.
|
|
451
|
-
* @returns {HeaderOptions} normalized options.
|
|
452
|
-
*/
|
|
453
|
-
function normalizeOptions(originalOptions) {
|
|
454
|
-
const options = structuredClone(originalOptions);
|
|
455
|
-
|
|
456
|
-
if (isFileBasedHeaderConfig(originalOptions.header)) {
|
|
457
|
-
const text = fs.readFileSync(originalOptions.header.file, originalOptions.header.encoding || "utf8");
|
|
466
|
+
function normalizeMatchingRules(matcher) {
|
|
467
|
+
if (isFileBasedHeaderConfig(matcher)) {
|
|
468
|
+
const text = fs.readFileSync(matcher.file, matcher.encoding || "utf8");
|
|
458
469
|
const [commentType, lines] = commentParser(text);
|
|
459
|
-
|
|
470
|
+
return { commentType, lines };
|
|
460
471
|
}
|
|
461
|
-
|
|
462
|
-
|
|
472
|
+
const commentType = matcher.commentType;
|
|
473
|
+
const lines = matcher.lines.flatMap(
|
|
463
474
|
(line) => {
|
|
464
475
|
if (typeof line === "string") {
|
|
465
476
|
return /** @type {HeaderLine[]} */(line.split(/\r?\n/));
|
|
@@ -477,6 +488,19 @@ function normalizeOptions(originalOptions) {
|
|
|
477
488
|
}
|
|
478
489
|
return [{ pattern }];
|
|
479
490
|
});
|
|
491
|
+
return { commentType, lines };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Transforms a set of new-style options adding defaults and standardizing on
|
|
496
|
+
* one of multiple config styles.
|
|
497
|
+
* @param {HeaderOptions} originalOptions New-style options to normalize.
|
|
498
|
+
* @returns {HeaderOptions} Normalized options.
|
|
499
|
+
*/
|
|
500
|
+
function normalizeOptions(originalOptions) {
|
|
501
|
+
const options = structuredClone(originalOptions);
|
|
502
|
+
|
|
503
|
+
options.header = normalizeMatchingRules(originalOptions.header);
|
|
480
504
|
|
|
481
505
|
if (!options.lineEndings) {
|
|
482
506
|
options.lineEndings = "os";
|
|
@@ -498,17 +522,15 @@ function normalizeOptions(originalOptions) {
|
|
|
498
522
|
* insufficient) lines that trail the comment. A special case is when there are
|
|
499
523
|
* no empty lines after the header in which case we highlight the next character
|
|
500
524
|
* in the source regardless of which one it is).
|
|
501
|
-
* @param {Comment[]} leadingComments
|
|
502
|
-
*
|
|
503
|
-
* @param {number} actualEmptyLines
|
|
504
|
-
*
|
|
505
|
-
* @returns {SourceLocation}
|
|
525
|
+
* @param {Comment[]} leadingComments The comment lines that constitute the
|
|
526
|
+
* header.
|
|
527
|
+
* @param {number} actualEmptyLines The number of empty lines that follow the
|
|
528
|
+
* header.
|
|
529
|
+
* @returns {SourceLocation} The location (line and column) of the violation.
|
|
506
530
|
*/
|
|
507
531
|
function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
|
|
508
532
|
assert.ok(leadingComments);
|
|
509
|
-
const loc = leadingComments[leadingComments.length - 1].loc;
|
|
510
|
-
assertDefined(loc);
|
|
511
|
-
assertNotNull(loc);
|
|
533
|
+
const loc = /** @type {SourceLocation} */ (leadingComments[leadingComments.length - 1].loc);
|
|
512
534
|
const lastCommentLineLocEnd = loc.end;
|
|
513
535
|
return {
|
|
514
536
|
start: lastCommentLineLocEnd,
|
|
@@ -519,7 +541,315 @@ function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
|
|
|
519
541
|
};
|
|
520
542
|
}
|
|
521
543
|
|
|
522
|
-
/**
|
|
544
|
+
/**
|
|
545
|
+
* Matches comments against of header content-matching rules. An object performs
|
|
546
|
+
* a number of expensive operations only once and thus can be used multiple
|
|
547
|
+
* times to test different comments.
|
|
548
|
+
*/
|
|
549
|
+
class CommentMatcher {
|
|
550
|
+
/**
|
|
551
|
+
* Initializes the matcher for a specific comment-matching rules.
|
|
552
|
+
* @param {InlineConfig} headerConfig Content-matching rules.
|
|
553
|
+
* @param {string} eol The EOL characters used.
|
|
554
|
+
* @param {number} numLines The requirred minimum number of trailing empty
|
|
555
|
+
* lines.
|
|
556
|
+
*/
|
|
557
|
+
constructor(headerConfig, eol, numLines) {
|
|
558
|
+
this.commentType = headerConfig.commentType;
|
|
559
|
+
this.headerLines = headerConfig.lines.map((line) => isPattern(line) ? line.pattern : line);
|
|
560
|
+
this.eol = eol;
|
|
561
|
+
this.numLines = numLines;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Performs a validation of a comment against a header matching
|
|
566
|
+
* configuration.
|
|
567
|
+
* @param {Comment[]} leadingComments The block comment or sequence of line
|
|
568
|
+
* comments to test.
|
|
569
|
+
* @param {SourceCode} sourceCode The source code AST.
|
|
570
|
+
* @returns {ViolationReport<JSSyntaxElement, string> | null} If set a
|
|
571
|
+
* violation report to pass back to ESLint or interpret as necessary.
|
|
572
|
+
*/
|
|
573
|
+
validate(leadingComments, sourceCode) {
|
|
574
|
+
|
|
575
|
+
const firstLeadingCommentLoc = /** @type {SourceLocation} */ (leadingComments[0].loc);
|
|
576
|
+
const firstLeadingCommentRange = /** @type {[number, number]} */ (leadingComments[0].range);
|
|
577
|
+
|
|
578
|
+
const lastLeadingCommentLoc = /** @type {SourceLocation} */ (leadingComments[leadingComments.length - 1].loc);
|
|
579
|
+
|
|
580
|
+
if (leadingComments[0].type.toLowerCase() !== this.commentType) {
|
|
581
|
+
return {
|
|
582
|
+
loc: {
|
|
583
|
+
start: firstLeadingCommentLoc.start,
|
|
584
|
+
end: lastLeadingCommentLoc.end
|
|
585
|
+
},
|
|
586
|
+
messageId: "incorrectCommentType",
|
|
587
|
+
data: {
|
|
588
|
+
commentType: this.commentType
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (this.commentType === commentTypeOptions.line) {
|
|
593
|
+
if (this.headerLines.length === 1) {
|
|
594
|
+
const leadingCommentValues = leadingComments.map((c) => c.value);
|
|
595
|
+
if (
|
|
596
|
+
!match(leadingCommentValues.join("\n"), this.headerLines[0])
|
|
597
|
+
&& !match(leadingCommentValues.join("\r\n"), this.headerLines[0])
|
|
598
|
+
) {
|
|
599
|
+
return {
|
|
600
|
+
loc: {
|
|
601
|
+
start: firstLeadingCommentLoc.start,
|
|
602
|
+
end: lastLeadingCommentLoc.end
|
|
603
|
+
},
|
|
604
|
+
messageId: "incorrectHeader",
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
for (let i = 0; i < this.headerLines.length; i++) {
|
|
609
|
+
if (leadingComments.length - 1 < i) {
|
|
610
|
+
return {
|
|
611
|
+
loc: {
|
|
612
|
+
start: lastLeadingCommentLoc.end,
|
|
613
|
+
end: lastLeadingCommentLoc.end
|
|
614
|
+
},
|
|
615
|
+
messageId: "headerTooShort",
|
|
616
|
+
data: {
|
|
617
|
+
remainder: this.headerLines.slice(i).join(this.eol)
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const headerLine = this.headerLines[i];
|
|
622
|
+
const comment = leadingComments[i];
|
|
623
|
+
const commentLoc = /** @type {SourceLocation} */ (comment.loc);
|
|
624
|
+
if (typeof headerLine === "string") {
|
|
625
|
+
const leadingCommentLength = comment.value.length;
|
|
626
|
+
const headerLineLength = headerLine.length;
|
|
627
|
+
for (let j = 0; j < Math.min(leadingCommentLength, headerLineLength); j++) {
|
|
628
|
+
if (comment.value[j] !== headerLine[j]) {
|
|
629
|
+
return {
|
|
630
|
+
loc: {
|
|
631
|
+
start: {
|
|
632
|
+
column: "//".length + j,
|
|
633
|
+
line: commentLoc.start.line
|
|
634
|
+
},
|
|
635
|
+
end: commentLoc.end
|
|
636
|
+
},
|
|
637
|
+
messageId: "headerLineMismatchAtPos",
|
|
638
|
+
data: {
|
|
639
|
+
expected: headerLine.substring(j)
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (leadingCommentLength < headerLineLength) {
|
|
645
|
+
return {
|
|
646
|
+
loc: {
|
|
647
|
+
start: commentLoc.end,
|
|
648
|
+
end: commentLoc.end,
|
|
649
|
+
},
|
|
650
|
+
messageId: "headerLineTooShort",
|
|
651
|
+
data: {
|
|
652
|
+
remainder: headerLine.substring(leadingCommentLength)
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
if (leadingCommentLength > headerLineLength) {
|
|
657
|
+
return {
|
|
658
|
+
loc: {
|
|
659
|
+
start: {
|
|
660
|
+
column: "//".length + headerLineLength,
|
|
661
|
+
line: commentLoc.start.line
|
|
662
|
+
},
|
|
663
|
+
end: commentLoc.end,
|
|
664
|
+
},
|
|
665
|
+
messageId: "headerLineTooLong",
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
if (!match(comment.value, headerLine)) {
|
|
670
|
+
return {
|
|
671
|
+
loc: {
|
|
672
|
+
start: {
|
|
673
|
+
column: "//".length,
|
|
674
|
+
line: commentLoc.start.line,
|
|
675
|
+
},
|
|
676
|
+
end: commentLoc.end,
|
|
677
|
+
},
|
|
678
|
+
messageId: "incorrectHeaderLine",
|
|
679
|
+
data: {
|
|
680
|
+
pattern: headerLine.toString()
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const commentRange = /** @type {[number, number]} */ (leadingComments[this.headerLines.length - 1].range);
|
|
689
|
+
const actualLeadingEmptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
|
|
690
|
+
const missingEmptyLines = this.numLines - actualLeadingEmptyLines;
|
|
691
|
+
if (missingEmptyLines > 0) {
|
|
692
|
+
return {
|
|
693
|
+
loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
|
|
694
|
+
messageId: "noNewlineAfterHeader",
|
|
695
|
+
data: {
|
|
696
|
+
expected: this.numLines,
|
|
697
|
+
actual: actualLeadingEmptyLines
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
// if block comment pattern has more than 1 line, we also split the
|
|
705
|
+
// comment
|
|
706
|
+
let leadingLines = [leadingComments[0].value];
|
|
707
|
+
if (this.headerLines.length > 1) {
|
|
708
|
+
leadingLines = leadingComments[0].value.split(/\r?\n/);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** @type {null | string} */
|
|
712
|
+
let errorMessageId = null;
|
|
713
|
+
/** @type {undefined | Record<string, string>} */
|
|
714
|
+
let errorMessageData;
|
|
715
|
+
/** @type {null | SourceLocation} */
|
|
716
|
+
let errorMessageLoc = null;
|
|
717
|
+
for (let i = 0; i < this.headerLines.length; i++) {
|
|
718
|
+
if (leadingLines.length - 1 < i) {
|
|
719
|
+
return {
|
|
720
|
+
loc: {
|
|
721
|
+
start: lastLeadingCommentLoc.end,
|
|
722
|
+
end: lastLeadingCommentLoc.end
|
|
723
|
+
},
|
|
724
|
+
messageId: "headerTooShort",
|
|
725
|
+
data: {
|
|
726
|
+
remainder: this.headerLines.slice(i).join(this.eol)
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const leadingLine = leadingLines[i];
|
|
731
|
+
const headerLine = this.headerLines[i];
|
|
732
|
+
if (typeof headerLine === "string") {
|
|
733
|
+
for (let j = 0; j < Math.min(leadingLine.length, headerLine.length); j++) {
|
|
734
|
+
if (leadingLine[j] !== headerLine[j]) {
|
|
735
|
+
errorMessageId = "headerLineMismatchAtPos";
|
|
736
|
+
const columnOffset = i === 0 ? "/*".length : 0;
|
|
737
|
+
const line = firstLeadingCommentLoc.start.line + i;
|
|
738
|
+
errorMessageLoc = {
|
|
739
|
+
start: {
|
|
740
|
+
column: columnOffset + j,
|
|
741
|
+
line
|
|
742
|
+
},
|
|
743
|
+
end: {
|
|
744
|
+
column: columnOffset + leadingLine.length,
|
|
745
|
+
line
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
errorMessageData = {
|
|
749
|
+
expected: headerLine.substring(j)
|
|
750
|
+
};
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (errorMessageId) {
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
if (leadingLine.length < headerLine.length) {
|
|
758
|
+
errorMessageId = "headerLineTooShort";
|
|
759
|
+
const startColumn = (i === 0 ? "/*".length : 0) + leadingLine.length;
|
|
760
|
+
errorMessageLoc = {
|
|
761
|
+
start: {
|
|
762
|
+
column: startColumn,
|
|
763
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
764
|
+
},
|
|
765
|
+
end: {
|
|
766
|
+
column: startColumn + 1,
|
|
767
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
errorMessageData = {
|
|
771
|
+
remainder: headerLine.substring(leadingLine.length)
|
|
772
|
+
};
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
if (leadingLine.length > headerLine.length) {
|
|
776
|
+
errorMessageId = "headerLineTooLong";
|
|
777
|
+
errorMessageLoc = {
|
|
778
|
+
start: {
|
|
779
|
+
column: (i === 0 ? "/*".length : 0) + headerLine.length,
|
|
780
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
781
|
+
},
|
|
782
|
+
end: {
|
|
783
|
+
column: (i === 0 ? "/*".length : 0) + leadingLine.length,
|
|
784
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
if (!match(leadingLine, headerLine)) {
|
|
791
|
+
errorMessageId = "incorrectHeaderLine";
|
|
792
|
+
errorMessageData = {
|
|
793
|
+
pattern: headerLine.toString()
|
|
794
|
+
};
|
|
795
|
+
const columnOffset = i === 0 ? "/*".length : 0;
|
|
796
|
+
errorMessageLoc = {
|
|
797
|
+
start: {
|
|
798
|
+
column: columnOffset + 0,
|
|
799
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
800
|
+
},
|
|
801
|
+
end: {
|
|
802
|
+
column: columnOffset + leadingLine.length,
|
|
803
|
+
line: firstLeadingCommentLoc.start.line + i
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (!errorMessageId && leadingLines.length > this.headerLines.length) {
|
|
812
|
+
errorMessageId = "headerTooLong";
|
|
813
|
+
errorMessageLoc = {
|
|
814
|
+
start: {
|
|
815
|
+
column: (this.headerLines.length === 0 ? "/*".length : 0) + 0,
|
|
816
|
+
line: firstLeadingCommentLoc.start.line + this.headerLines.length
|
|
817
|
+
},
|
|
818
|
+
end: {
|
|
819
|
+
column: lastLeadingCommentLoc.end.column - "*/".length,
|
|
820
|
+
line: lastLeadingCommentLoc.end.line
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (errorMessageId) {
|
|
826
|
+
return {
|
|
827
|
+
loc: /** @type {SourceLocation} */ (errorMessageLoc),
|
|
828
|
+
messageId: errorMessageId,
|
|
829
|
+
data: errorMessageData,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const actualLeadingEmptyLines =
|
|
834
|
+
leadingEmptyLines(sourceCode.text.substring(firstLeadingCommentRange[1]));
|
|
835
|
+
const missingEmptyLines = this.numLines - actualLeadingEmptyLines;
|
|
836
|
+
if (missingEmptyLines > 0) {
|
|
837
|
+
return {
|
|
838
|
+
loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
|
|
839
|
+
messageId: "noNewlineAfterHeader",
|
|
840
|
+
data: {
|
|
841
|
+
expected: this.numLines,
|
|
842
|
+
actual: actualLeadingEmptyLines
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
/** @type {Rule.RuleModule} */
|
|
523
853
|
const headerRule = {
|
|
524
854
|
meta: {
|
|
525
855
|
type: "layout",
|
|
@@ -551,55 +881,43 @@ const headerRule = {
|
|
|
551
881
|
noNewlineAfterHeader: "not enough newlines after header: expected: {{expected}}, actual: {{actual}}"
|
|
552
882
|
}
|
|
553
883
|
},
|
|
884
|
+
|
|
554
885
|
/**
|
|
555
886
|
* Rule creation function.
|
|
556
887
|
* @param {RuleContext} context ESLint rule execution context.
|
|
557
|
-
* @returns {NodeListener}
|
|
888
|
+
* @returns {NodeListener} The rule definition.
|
|
558
889
|
*/
|
|
559
|
-
create: function(context) {
|
|
890
|
+
create: function (context) {
|
|
560
891
|
|
|
561
|
-
const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */
|
|
892
|
+
const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */(context.options));
|
|
562
893
|
const options = normalizeOptions(newStyleOptions);
|
|
563
894
|
|
|
564
|
-
assertLineBasedHeaderConfig(options.header);
|
|
565
|
-
const commentType = /** @type {CommentType} */ (options.header.commentType);
|
|
566
|
-
|
|
567
895
|
const eol = getEol(
|
|
568
|
-
/** @type {LineEndingOption} */
|
|
896
|
+
/** @type {LineEndingOption} */(options.lineEndings)
|
|
569
897
|
);
|
|
570
898
|
|
|
571
|
-
/** @type {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
let canFix = true;
|
|
577
|
-
const headerLines = options.header.lines.map(function(line) {
|
|
578
|
-
// Can only fix regex option if a template is also provided
|
|
899
|
+
const header = /** @type {InlineConfig} */ (options.header);
|
|
900
|
+
|
|
901
|
+
const canFix = !header.lines.some((line) => isPattern(line) && !("template" in line));
|
|
902
|
+
|
|
903
|
+
const fixLines = header.lines.map((line) => {
|
|
579
904
|
if (isPattern(line)) {
|
|
580
|
-
|
|
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;
|
|
905
|
+
return ("template" in line) ? /** @type {string} */(line.template) : "";
|
|
590
906
|
}
|
|
907
|
+
return /** @type {string} */(line);
|
|
591
908
|
});
|
|
592
909
|
|
|
593
|
-
|
|
594
910
|
const numLines = /** @type {number} */ (options.trailingEmptyLines?.minimum);
|
|
595
911
|
|
|
912
|
+
const headerMatcher = new CommentMatcher(header, eol, numLines);
|
|
913
|
+
|
|
596
914
|
return {
|
|
597
915
|
/**
|
|
598
916
|
* Hooks into the processing of the overall script node to do the
|
|
599
917
|
* header validation.
|
|
600
918
|
* @returns {void}
|
|
601
919
|
*/
|
|
602
|
-
Program: function() {
|
|
920
|
+
Program: function () {
|
|
603
921
|
const sourceCode = contextSourceCode(context);
|
|
604
922
|
if (!hasHeader(sourceCode.text)) {
|
|
605
923
|
const hasShebang = sourceCode.text.startsWith("#!");
|
|
@@ -616,41 +934,10 @@ const headerRule = {
|
|
|
616
934
|
}
|
|
617
935
|
},
|
|
618
936
|
messageId: "missingHeader",
|
|
619
|
-
fix: genPrependFixer(
|
|
620
|
-
commentType,
|
|
621
|
-
context,
|
|
622
|
-
fixLines,
|
|
623
|
-
eol,
|
|
624
|
-
numLines)
|
|
625
|
-
});
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
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);
|
|
640
|
-
context.report({
|
|
641
|
-
loc: {
|
|
642
|
-
start: firstLeadingCommentLoc.start,
|
|
643
|
-
end: lastLeadingCommentLoc.end
|
|
644
|
-
},
|
|
645
|
-
messageId: "incorrectCommentType",
|
|
646
|
-
data: {
|
|
647
|
-
commentType: commentType
|
|
648
|
-
},
|
|
649
937
|
fix: canFix
|
|
650
|
-
?
|
|
651
|
-
commentType,
|
|
652
|
-
|
|
653
|
-
leadingComments,
|
|
938
|
+
? genPrependFixer(
|
|
939
|
+
headerMatcher.commentType,
|
|
940
|
+
sourceCode,
|
|
654
941
|
fixLines,
|
|
655
942
|
eol,
|
|
656
943
|
numLines)
|
|
@@ -658,341 +945,25 @@ const headerRule = {
|
|
|
658
945
|
});
|
|
659
946
|
return;
|
|
660
947
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
fix: canFix
|
|
679
|
-
? genReplaceFixer(
|
|
680
|
-
commentType,
|
|
681
|
-
context,
|
|
682
|
-
leadingComments,
|
|
683
|
-
fixLines,
|
|
684
|
-
eol,
|
|
685
|
-
numLines)
|
|
686
|
-
: null
|
|
687
|
-
});
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
} else {
|
|
691
|
-
for (let i = 0; i < headerLines.length; i++) {
|
|
692
|
-
if (leadingComments.length - 1 < i) {
|
|
693
|
-
assertDefined(lastLeadingCommentLoc);
|
|
694
|
-
assertNotNull(lastLeadingCommentLoc);
|
|
695
|
-
context.report({
|
|
696
|
-
loc: {
|
|
697
|
-
start: lastLeadingCommentLoc.end,
|
|
698
|
-
end: lastLeadingCommentLoc.end
|
|
699
|
-
},
|
|
700
|
-
messageId: "headerTooShort",
|
|
701
|
-
data: {
|
|
702
|
-
remainder: headerLines.slice(i).join(eol)
|
|
703
|
-
},
|
|
704
|
-
fix: canFix
|
|
705
|
-
? genReplaceFixer(
|
|
706
|
-
commentType,
|
|
707
|
-
context,
|
|
708
|
-
leadingComments,
|
|
709
|
-
fixLines,
|
|
710
|
-
eol,
|
|
711
|
-
numLines)
|
|
712
|
-
: null
|
|
713
|
-
});
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
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;
|
|
724
|
-
for (let j = 0; j < Math.min(leadingCommentLength, headerLineLength); j++) {
|
|
725
|
-
if (comment.value[j] !== headerLine[j]) {
|
|
726
|
-
context.report({
|
|
727
|
-
loc: {
|
|
728
|
-
start: {
|
|
729
|
-
column: "//".length + j,
|
|
730
|
-
line: commentLoc.start.line
|
|
731
|
-
},
|
|
732
|
-
end: commentLoc.end
|
|
733
|
-
},
|
|
734
|
-
messageId: "headerLineMismatchAtPos",
|
|
735
|
-
data: {
|
|
736
|
-
expected: headerLine.substring(j)
|
|
737
|
-
},
|
|
738
|
-
fix: genReplaceFixer(
|
|
739
|
-
commentType,
|
|
740
|
-
context,
|
|
741
|
-
leadingComments,
|
|
742
|
-
fixLines,
|
|
743
|
-
eol,
|
|
744
|
-
numLines)
|
|
745
|
-
});
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
if (leadingCommentLength < headerLineLength) {
|
|
750
|
-
context.report({
|
|
751
|
-
loc: {
|
|
752
|
-
start: commentLoc.end,
|
|
753
|
-
end: commentLoc.end,
|
|
754
|
-
},
|
|
755
|
-
messageId: "headerLineTooShort",
|
|
756
|
-
data: {
|
|
757
|
-
remainder: headerLine.substring(leadingCommentLength)
|
|
758
|
-
},
|
|
759
|
-
fix: canFix
|
|
760
|
-
? genReplaceFixer(
|
|
761
|
-
commentType,
|
|
762
|
-
context,
|
|
763
|
-
leadingComments,
|
|
764
|
-
fixLines,
|
|
765
|
-
eol,
|
|
766
|
-
numLines)
|
|
767
|
-
: null
|
|
768
|
-
});
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
if (leadingCommentLength > headerLineLength) {
|
|
772
|
-
context.report({
|
|
773
|
-
loc: {
|
|
774
|
-
start: {
|
|
775
|
-
column: "//".length + headerLineLength,
|
|
776
|
-
line: commentLoc.start.line
|
|
777
|
-
},
|
|
778
|
-
end: commentLoc.end,
|
|
779
|
-
},
|
|
780
|
-
messageId: "headerLineTooLong",
|
|
781
|
-
fix: canFix
|
|
782
|
-
? genReplaceFixer(
|
|
783
|
-
commentType,
|
|
784
|
-
context,
|
|
785
|
-
leadingComments,
|
|
786
|
-
fixLines,
|
|
787
|
-
eol,
|
|
788
|
-
numLines)
|
|
789
|
-
: null
|
|
790
|
-
});
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
} else {
|
|
794
|
-
if (!match(comment.value, headerLine)) {
|
|
795
|
-
context.report({
|
|
796
|
-
loc: {
|
|
797
|
-
start: {
|
|
798
|
-
column: "//".length,
|
|
799
|
-
line: commentLoc.start.line,
|
|
800
|
-
},
|
|
801
|
-
end: commentLoc.end,
|
|
802
|
-
},
|
|
803
|
-
messageId: "incorrectHeaderLine",
|
|
804
|
-
data: {
|
|
805
|
-
pattern: headerLine.toString()
|
|
806
|
-
},
|
|
807
|
-
fix: canFix
|
|
808
|
-
? genReplaceFixer(
|
|
809
|
-
commentType,
|
|
810
|
-
context,
|
|
811
|
-
leadingComments,
|
|
812
|
-
fixLines,
|
|
813
|
-
eol,
|
|
814
|
-
numLines)
|
|
815
|
-
: null
|
|
816
|
-
});
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
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;
|
|
827
|
-
if (missingEmptyLines > 0) {
|
|
828
|
-
context.report({
|
|
829
|
-
loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
|
|
830
|
-
messageId: "noNewlineAfterHeader",
|
|
831
|
-
data: {
|
|
832
|
-
expected: numLines,
|
|
833
|
-
actual: actualLeadingEmptyLines
|
|
834
|
-
},
|
|
835
|
-
fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
// if block comment pattern has more than 1 line, we also split
|
|
841
|
-
// the comment
|
|
842
|
-
let leadingLines = [leadingComments[0].value];
|
|
843
|
-
if (headerLines.length > 1) {
|
|
844
|
-
leadingLines = leadingComments[0].value.split(/\r?\n/);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/** @type {null | string} */
|
|
848
|
-
let errorMessageId = null;
|
|
849
|
-
/** @type {undefined | Record<string, string>} */
|
|
850
|
-
let errorMessageData;
|
|
851
|
-
/** @type {null | SourceLocation} */
|
|
852
|
-
let errorMessageLoc = null;
|
|
853
|
-
for (let i = 0; i < headerLines.length; i++) {
|
|
854
|
-
const leadingLine = leadingLines[i];
|
|
855
|
-
const headerLine = headerLines[i];
|
|
856
|
-
if (typeof headerLine === "string") {
|
|
857
|
-
for (let j = 0; j < Math.min(leadingLine.length, headerLine.length); j++) {
|
|
858
|
-
if (leadingLine[j] !== headerLine[j]) {
|
|
859
|
-
errorMessageId = "headerLineMismatchAtPos";
|
|
860
|
-
const columnOffset = i === 0 ? "/*".length : 0;
|
|
861
|
-
assertDefined(firstLeadingCommentLoc);
|
|
862
|
-
assertNotNull(firstLeadingCommentLoc);
|
|
863
|
-
const line = firstLeadingCommentLoc.start.line + i;
|
|
864
|
-
errorMessageLoc = {
|
|
865
|
-
start: {
|
|
866
|
-
column: columnOffset + j,
|
|
867
|
-
line
|
|
868
|
-
},
|
|
869
|
-
end: {
|
|
870
|
-
column: columnOffset + leadingLine.length,
|
|
871
|
-
line
|
|
872
|
-
}
|
|
873
|
-
};
|
|
874
|
-
errorMessageData = {
|
|
875
|
-
expected: headerLine.substring(j)
|
|
876
|
-
};
|
|
877
|
-
break;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (errorMessageId) {
|
|
881
|
-
break;
|
|
882
|
-
}
|
|
883
|
-
if (leadingLine.length < headerLine.length) {
|
|
884
|
-
errorMessageId = "headerLineTooShort";
|
|
885
|
-
const startColumn = (i === 0 ? "/*".length : 0) + leadingLine.length;
|
|
886
|
-
assertDefined(firstLeadingCommentLoc);
|
|
887
|
-
assertNotNull(firstLeadingCommentLoc);
|
|
888
|
-
errorMessageLoc = {
|
|
889
|
-
start: {
|
|
890
|
-
column: startColumn,
|
|
891
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
892
|
-
},
|
|
893
|
-
end: {
|
|
894
|
-
column: startColumn + 1,
|
|
895
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
896
|
-
}
|
|
897
|
-
};
|
|
898
|
-
errorMessageData = {
|
|
899
|
-
remainder: headerLine.substring(leadingLine.length)
|
|
900
|
-
};
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
903
|
-
if (leadingLine.length > headerLine.length) {
|
|
904
|
-
assertDefined(firstLeadingCommentLoc);
|
|
905
|
-
assertNotNull(firstLeadingCommentLoc);
|
|
906
|
-
errorMessageId = "headerLineTooLong";
|
|
907
|
-
errorMessageLoc = {
|
|
908
|
-
start: {
|
|
909
|
-
column: (i === 0 ? "/*".length : 0) + headerLine.length,
|
|
910
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
911
|
-
},
|
|
912
|
-
end: {
|
|
913
|
-
column: (i === 0 ? "/*".length : 0) + leadingLine.length,
|
|
914
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
915
|
-
}
|
|
916
|
-
};
|
|
917
|
-
break;
|
|
918
|
-
}
|
|
919
|
-
} else {
|
|
920
|
-
if (!match(leadingLine, headerLine)) {
|
|
921
|
-
errorMessageId = "incorrectHeaderLine";
|
|
922
|
-
errorMessageData = {
|
|
923
|
-
pattern: headerLine.toString()
|
|
924
|
-
};
|
|
925
|
-
const columnOffset = i === 0 ? "/*".length : 0;
|
|
926
|
-
assertDefined(firstLeadingCommentLoc);
|
|
927
|
-
assertNotNull(firstLeadingCommentLoc);
|
|
928
|
-
errorMessageLoc = {
|
|
929
|
-
start: {
|
|
930
|
-
column: columnOffset + 0,
|
|
931
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
932
|
-
},
|
|
933
|
-
end: {
|
|
934
|
-
column: columnOffset + leadingLine.length,
|
|
935
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
break;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
if (!errorMessageId && leadingLines.length > headerLines.length) {
|
|
944
|
-
errorMessageId = "headerTooLong";
|
|
945
|
-
assertDefined(firstLeadingCommentLoc);
|
|
946
|
-
assertNotNull(firstLeadingCommentLoc);
|
|
947
|
-
assertDefined(lastLeadingCommentLoc);
|
|
948
|
-
assertNotNull(lastLeadingCommentLoc);
|
|
949
|
-
errorMessageLoc = {
|
|
950
|
-
start: {
|
|
951
|
-
column: (headerLines.length === 0 ? "/*".length : 0) + 0,
|
|
952
|
-
line: firstLeadingCommentLoc.start.line + headerLines.length
|
|
953
|
-
},
|
|
954
|
-
end: {
|
|
955
|
-
column: lastLeadingCommentLoc.end.column - "*/".length,
|
|
956
|
-
line: lastLeadingCommentLoc.end.line
|
|
957
|
-
}
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (errorMessageId) {
|
|
962
|
-
if (canFix && headerLines.length > 1) {
|
|
963
|
-
fixLines = [fixLines.join(eol)];
|
|
948
|
+
const leadingComments = getLeadingComments(sourceCode);
|
|
949
|
+
|
|
950
|
+
const report = headerMatcher.validate(leadingComments, sourceCode);
|
|
951
|
+
|
|
952
|
+
if (report !== null) {
|
|
953
|
+
if ("messageId" in report && report.messageId === "noNewlineAfterHeader") {
|
|
954
|
+
const { expected, actual } =
|
|
955
|
+
/** @type {{ expected: number, actual: number }} */ (report.data);
|
|
956
|
+
report.fix = genEmptyLinesFixer(leadingComments, eol, expected - actual);
|
|
957
|
+
} else if (canFix) {
|
|
958
|
+
report.fix = genReplaceFixer(
|
|
959
|
+
headerMatcher.commentType,
|
|
960
|
+
sourceCode,
|
|
961
|
+
leadingComments,
|
|
962
|
+
fixLines,
|
|
963
|
+
eol,
|
|
964
|
+
numLines);
|
|
964
965
|
}
|
|
965
|
-
|
|
966
|
-
context.report({
|
|
967
|
-
loc: errorMessageLoc,
|
|
968
|
-
messageId: errorMessageId,
|
|
969
|
-
data: errorMessageData,
|
|
970
|
-
fix: canFix
|
|
971
|
-
? genReplaceFixer(
|
|
972
|
-
commentType,
|
|
973
|
-
context,
|
|
974
|
-
leadingComments,
|
|
975
|
-
fixLines,
|
|
976
|
-
eol,
|
|
977
|
-
numLines)
|
|
978
|
-
: null
|
|
979
|
-
});
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
const actualLeadingEmptyLines =
|
|
984
|
-
leadingEmptyLines(sourceCode.text.substring(firstLeadingCommentRange[1]));
|
|
985
|
-
const missingEmptyLines = numLines - actualLeadingEmptyLines;
|
|
986
|
-
if (missingEmptyLines > 0) {
|
|
987
|
-
context.report({
|
|
988
|
-
loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
|
|
989
|
-
messageId: "noNewlineAfterHeader",
|
|
990
|
-
data: {
|
|
991
|
-
expected: numLines,
|
|
992
|
-
actual: actualLeadingEmptyLines
|
|
993
|
-
},
|
|
994
|
-
fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
|
|
995
|
-
});
|
|
966
|
+
context.report(report);
|
|
996
967
|
}
|
|
997
968
|
}
|
|
998
969
|
};
|