@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.
@@ -1,8 +1,8 @@
1
- /*
2
- * MIT License
3
- *
4
- * Copyright (c) 2015-present Stuart Knightley, Tony Ganchev, and contributors
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
- * Import type definitions.
37
- * @typedef {import('eslint').Rule.Fix} Fix
38
- * @typedef {import('eslint').Rule.NodeListener} NodeListener
39
- * @typedef {import('eslint').Rule.ReportFixer} ReportFixer
40
- * @typedef {import('eslint').Rule.RuleFixer} RuleFixer
41
- * @typedef {import('eslint').Rule.RuleContext} RuleContext
42
- * @typedef {import('estree').Comment} Comment
43
- * @typedef {import('estree').Program} Program
44
- * @typedef {import("estree").SourceLocation} SourceLocation
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
- * Local type definitions.
49
- * @typedef {{ pattern: string | RegExp, template?: string }} HeaderLinePattern
50
- * @typedef {string | RegExp | HeaderLinePattern} HeaderLine
51
- * @typedef {HeaderLine | HeaderLine[]} HeaderLines
52
- * @typedef {'os' | 'unix' | 'windows'} LineEndingOption
53
- * @typedef {{ lineEndings?: LineEndingOption }} HeaderSettings
54
- * @typedef {'block' | 'line'} CommentType
55
- * @typedef {{
56
- * file: string,
57
- * encoding?: BufferEncoding
58
- * }
59
- * } FileBasedConfig
60
- * @typedef {{
61
- * commentType: CommentType,
62
- * lines: HeaderLine[]
63
- * }
64
- * } InlineConfig
65
- * @typedef {{ minimum?: number }} TrailingEmptyLines
66
- * @typedef {{
67
- * header: FileBasedConfig | InlineConfig,
68
- * trailingEmptyLines?: TrailingEmptyLines
69
- * }
70
- * & HeaderSettings
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
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 line configuration object or string
145
+ * @param {HeaderLine} object Line configuration object or string.
93
146
  * @returns {object is HeaderLinePattern} `true` if the line configuration is a
94
- * pattern-defining object or `false`
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 the string to test.
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
- * config or `false` otherwise.
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 the list of comment
121
- * lines.
122
- * @returns {Comment[]} the list of comments with containing all incoming
123
- * comments from `comments` with the shebang comments
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 {RuleContext} context ESLint execution environment.
159
- * @returns {Comment[]} lines that constitute the leading comment.
189
+ * @param {SourceCode} sourceCode AST.
190
+ * @returns {Comment[]} Lines That constitute the leading comment.
160
191
  */
161
- function getLeadingComments(context) {
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
- assertDefined(previousRange);
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 the type of comment to generate.
187
- * @param {string[]} textArray list of lines of the comment content.
188
- * @param {'\n' | '\r\n'} eol end-of-line characters.
189
- * @returns {string} resulting comment.
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 list of comments.
205
- * @returns {[number, number]} resulting range.
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
- assertDefined(firstComment);
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
- assertDefined(lastComment);
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 the source code to traverse.
226
- * @returns {number} the number of leading empty lines.
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 type of comment to use.
245
- * @param {RuleContext} context ESLint execution runtime.
246
- * @param {string[]} headerLines lines of the header comment.
247
- * @param {'\n' | '\r\n'} eol end-of-line characters
248
- * @param {number} numNewlines number of trailing lines after the comment.
249
- * @returns {ReportFixer} the fixer.
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, context, headerLines, eol, numNewlines) {
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 type of comment to use.
276
- * @param {RuleContext} context ESLint execution context.
277
- * @param {Comment[]} leadingComments comment elements to replace.
278
- * @param {string[]} headerLines lines of the header comment.
279
- * @param {'\n' | '\r\n'} eol end-of-line characters
280
- * @param {number} numNewlines number of trailing lines after the comment.
281
- * @returns {(fixer: RuleFixer) => Fix | Fix[] | null} the fixer.
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, context, leadingComments, headerLines, eol, numNewlines) {
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(contextSourceCode(context).text.substring(commentRange[1]));
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 comment elements to replace.
299
- * @param {'\n' | '\r\n'} eol end-of-line characters
300
- * @param {number} missingEmptyLinesCount number of trailing lines after the
301
- * comment.
302
- * @returns {(fixer: RuleFixer) => Fix | Fix[] | null} the fixer.
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 line-ending styles.
317
- * @returns {'\n' | '\r\n'} the correct line ending characters for the
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 {'\n' | '\r\n'} */ (os.EOL);
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 source code to test.
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
- * asserts on an expression and adds template texts to the failure message.
366
+ * Asserts on an expression and adds template texts to the failure message.
347
367
  * Helper to write cleaner code.
348
- * @param {boolean} condition assert condition.
349
- * @param {string} message assert message on violation.
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 the options as configured by the
363
- * user.
364
- * @returns {HeaderOptions} the transformed new-style options with no
365
- * normalization.
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
- * @param {FileBasedConfig | InlineConfig} config the header configuration.
432
- * @returns {config is FileBasedConfig} true if `config` is `FileBasedConfig`.
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
- * @param {FileBasedConfig | InlineConfig} config the header configuration.
440
- * @returns {asserts config is InlineConfig} asserts `config` is
441
- * `LineBasedConfig`.
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 assertLineBasedHeaderConfig(config) {
444
- assert.ok(Object.prototype.hasOwnProperty.call(config, "lines"));
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
- options.header = { commentType, lines };
470
+ return { commentType, lines };
460
471
  }
461
- assertLineBasedHeaderConfig(options.header);
462
- options.header.lines = options.header.lines.flatMap(
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 the comment lines that constitute the
502
- * header.
503
- * @param {number} actualEmptyLines the number of empty lines that follow the
504
- * header.
505
- * @returns {SourceLocation} the location (line and column) of the violation.
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
- /** @type {import('eslint').Rule.RuleModule} */
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} the rule definition.
888
+ * @returns {NodeListener} The rule definition.
558
889
  */
559
- create: function(context) {
890
+ create: function (context) {
560
891
 
561
- const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */ (context.options));
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} */ (options.lineEndings)
896
+ /** @type {LineEndingOption} */(options.lineEndings)
569
897
  );
570
898
 
571
- /** @type {string[]} */
572
- let fixLines = [];
573
- // If any of the lines are regular expressions, then we can't
574
- // automatically fix them. We set this to true below once we
575
- // ensure none of the lines are of type RegExp
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
- 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;
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
- ? genReplaceFixer(
651
- commentType,
652
- context,
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
- if (commentType === commentTypeOptions.line) {
662
- if (headerLines.length === 1) {
663
- const leadingCommentValues = leadingComments.map((c) => c.value);
664
- if (
665
- !match(leadingCommentValues.join("\n"), headerLines[0])
666
- && !match(leadingCommentValues.join("\r\n"), headerLines[0])
667
- ) {
668
- assertDefined(firstLeadingCommentLoc);
669
- assertNotNull(firstLeadingCommentLoc);
670
- assertDefined(lastLeadingCommentLoc);
671
- assertNotNull(lastLeadingCommentLoc);
672
- context.report({
673
- loc: {
674
- start: firstLeadingCommentLoc.start,
675
- end: lastLeadingCommentLoc.end
676
- },
677
- messageId: "incorrectHeader",
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
- assertNotNull(errorMessageLoc);
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
  };