@tony.ganchev/eslint-plugin-header 3.2.5 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,8 +33,9 @@ const { description, recommended } = require("./header.docs");
33
33
  const { lineEndingOptions, commentTypeOptions, schema } = require("./header.schema");
34
34
 
35
35
  /**
36
- * @import { Linter, Rule } from "eslint"
37
- * @import { Comment, SourceLocation, Program } from "estree"
36
+ * @import { JSSyntaxElement, Linter, Rule, SourceCode } from "eslint"
37
+ * @import { Comment, SourceLocation } from "estree"
38
+ * @import { ViolationReport } from "@eslint/core";
38
39
  * @typedef {Rule.NodeListener} NodeListener
39
40
  * @typedef {Rule.ReportFixer} ReportFixer
40
41
  * @typedef {Rule.RuleFixer} RuleFixer
@@ -88,6 +89,14 @@ const { lineEndingOptions, commentTypeOptions, schema } = require("./header.sche
88
89
  * lines together.
89
90
  */
90
91
 
92
+ /**
93
+ * @typedef {object} LeadingComments A set of comments that can appear before
94
+ * the header.
95
+ * @property {(FileBasedConfig | InlineConfig)[]} comments The set of comments
96
+ * that are allowed. If none of the matching rules matches the first comment the
97
+ * rule assumes the first comment *is* the header.
98
+ */
99
+
91
100
  /**
92
101
  * @typedef {object} TrailingEmptyLines Rule configuration on the handling of
93
102
  * empty lines after the header comment.
@@ -99,6 +108,9 @@ const { lineEndingOptions, commentTypeOptions, schema } = require("./header.sche
99
108
  * @typedef {object} HeaderOptionsWithoutSettings
100
109
  * @property {FileBasedConfig | InlineConfig} header The text matching rules
101
110
  * for the header.
111
+ * @property {LeadingComments} [leadingComments] The set of allowed comments to
112
+ * precede the header. Useful to allow position-sensitive pragma comments for
113
+ * certain tools.
102
114
  * @property {TrailingEmptyLines} [trailingEmptyLines] Rules about empty lines
103
115
  * after the header comment.
104
116
  */
@@ -167,67 +179,66 @@ function match(actual, expected) {
167
179
  }
168
180
 
169
181
  /**
170
- * Remove Unix she-bangs from the list of comments.
171
- * @param {(Comment | { type: "Shebang" })[]} comments The list of comment
172
- * lines.
173
- * @returns {Comment[]} The list of comments with containing all incoming
174
- * comments from `comments` with the shebang comments omitted.
182
+ * Returns an array of comment groups before the actual code.
183
+ * Block comments form single-element groups.
184
+ * Line comments without empty lines between them form grouped elements.
185
+ * @param {SourceCode} sourceCode AST.
186
+ * @returns {Comment[][]} Array of groups of leading comments.
175
187
  */
176
- function excludeShebangs(comments) {
188
+ function getLeadingComments(sourceCode) {
189
+ const all = sourceCode.getAllComments();
190
+ if (all.length === 0) {
191
+ return [];
192
+ }
193
+ // Determine where the actual code starts. If no code, use end of file.
194
+ const firstToken = sourceCode.getFirstToken(sourceCode.ast);
195
+ const codeStart = firstToken ? firstToken.range[0] : sourceCode.text.length;
196
+ // Filter comments that appear before the actual code starts.
197
+ const commentsBeforeCode = all.filter((c) => /** @type {[number, number]} */(c.range)[1] <= codeStart);
198
+ if (commentsBeforeCode.length === 0) {
199
+ return [];
200
+ }
201
+ /** @type {Comment[][]} */
202
+ const groups = [];
177
203
  /** @type {Comment[]} */
178
- return comments.filter(function(comment) {
179
- return comment.type !== "Shebang";
180
- });
181
- }
182
-
183
- /**
184
- * TypeScript helper to confirm defined type.
185
- * @template T Target type to validate for definiteness.
186
- * @param {T | undefined} val The value to validate.
187
- * @returns {asserts val is T} Validates defined type.
188
- */
189
- function assertDefined(val) {
190
- assert.strict.notEqual(typeof val, "undefined");
191
- }
204
+ let currentGroup = [];
205
+ for (let i = 0; i < commentsBeforeCode.length; ++i) {
206
+ const comment = commentsBeforeCode[i];
192
207
 
193
- /**
194
- * TypeScript helper to confirm non-null type.
195
- * @template T Target type to validate is non-null.
196
- * @param {T | null} val The value to validate.
197
- * @returns {asserts val is T} Validates non-null type.
198
- */
199
- function assertNotNull(val) {
200
- assert.strict.notEqual(val, null);
201
- }
208
+ if (comment.type === "Block") {
209
+ // Push any existing current group first
210
+ if (currentGroup.length > 0) {
211
+ groups.push(currentGroup);
212
+ currentGroup = [];
213
+ }
214
+ groups.push([comment]);
215
+ } else {
216
+ if (currentGroup.length === 0) {
217
+ currentGroup.push(comment);
218
+ } else {
219
+ const previous = currentGroup[currentGroup.length - 1];
220
+ const previousRange = /** @type {[number, number]} */ (previous.range);
221
+ const currentRange = /** @type {[number, number]} */ (comment.range);
222
+ const txt = sourceCode.text.slice(previousRange[1], currentRange[0]);
202
223
 
203
- /**
204
- * Returns either the first block comment or the first set of line comments that
205
- * are ONLY separated by a single newline. Note that this does not actually
206
- * check if they are at the start of the file since that is already checked by
207
- * `hasHeader()`.
208
- * @param {RuleContext} context ESLint execution environment.
209
- * @returns {Comment[]} Lines That constitute the leading comment.
210
- */
211
- function getLeadingComments(context) {
212
- const sourceCode = contextSourceCode(context);
213
- const all = excludeShebangs(sourceCode.getAllComments());
214
- assert.ok(all);
215
- assert.ok(all.length);
216
- if (all[0].type.toLowerCase() === commentTypeOptions.block) {
217
- return [all[0]];
218
- }
219
- let i = 1;
220
- for (; i < all.length; ++i) {
221
- const previousRange = all[i - 1].range;
222
- assertDefined(previousRange);
223
- const currentRange = all[i].range;
224
- assertDefined(currentRange);
225
- const txt = sourceCode.text.slice(previousRange[1], currentRange[0]);
226
- if (!txt.match(/^(\r\n|\r|\n)$/)) {
227
- break;
224
+ // If there is more than 1 newline, there is an empty line
225
+ // between comments.
226
+ const newlineCount = /** @type {RegExpMatchArray} */ (txt.match(/\r?\n/g)).length;
227
+ if (newlineCount <= 1) {
228
+ currentGroup.push(comment);
229
+ } else {
230
+ groups.push(currentGroup);
231
+ currentGroup = [comment];
232
+ }
233
+ }
228
234
  }
229
235
  }
230
- return all.slice(0, i);
236
+
237
+ if (currentGroup.length > 0) {
238
+ groups.push(currentGroup);
239
+ }
240
+
241
+ return groups;
231
242
  }
232
243
 
233
244
  /**
@@ -257,14 +268,10 @@ function genCommentBody(commentType, textArray, eol) {
257
268
  function genCommentsRange(comments) {
258
269
  assert.ok(comments.length);
259
270
  const firstComment = comments[0];
260
- assertDefined(firstComment);
261
- const firstCommentRange = firstComment.range;
262
- assertDefined(firstCommentRange);
271
+ const firstCommentRange = /** @type {[number, number]} */ (firstComment.range);
263
272
  const start = firstCommentRange[0];
264
273
  const lastComment = comments.slice(-1)[0];
265
- assertDefined(lastComment);
266
- const lastCommentRange = lastComment.range;
267
- assertDefined(lastCommentRange);
274
+ const lastCommentRange = /** @type {[number, number]} */ (lastComment.range);
268
275
  const end = lastCommentRange[1];
269
276
  return [start, end];
270
277
  }
@@ -292,17 +299,16 @@ function leadingEmptyLines(src) {
292
299
  /**
293
300
  * Factory for fixer that adds a missing header.
294
301
  * @param {CommentType} commentType Type of comment to use.
295
- * @param {RuleContext} context ESLint execution runtime.
302
+ * @param {SourceCode} sourceCode AST.
296
303
  * @param {string[]} headerLines Lines of the header comment.
297
304
  * @param {LineEnding} eol End-of-line characters.
298
305
  * @param {number} numNewlines Number of trailing lines after the comment.
299
306
  * @returns {ReportFixer} The fix to apply.
300
307
  */
301
- function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
302
- return function(fixer) {
308
+ function genPrependFixer(commentType, sourceCode, headerLines, eol, numNewlines) {
309
+ return function (fixer) {
303
310
  let insertPos = 0;
304
311
  let newHeader = genCommentBody(commentType, headerLines, eol);
305
- const sourceCode = contextSourceCode(context);
306
312
  if (sourceCode.text.startsWith("#!")) {
307
313
  const firstNewLinePos = sourceCode.text.indexOf("\n");
308
314
  insertPos = firstNewLinePos === -1 ? sourceCode.text.length : firstNewLinePos + 1;
@@ -323,17 +329,17 @@ function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
323
329
  /**
324
330
  * Factory for fixer that replaces an incorrect header.
325
331
  * @param {CommentType} commentType Type of comment to use.
326
- * @param {RuleContext} context ESLint execution context.
332
+ * @param {SourceCode} sourceCode AST.
327
333
  * @param {Comment[]} leadingComments Comment elements to replace.
328
334
  * @param {string[]} headerLines Lines of the header comment.
329
335
  * @param {LineEnding} eol End-of-line characters.
330
336
  * @param {number} numNewlines Number of trailing lines after the comment.
331
337
  * @returns {ReportFixer} The fix to apply.
332
338
  */
333
- function genReplaceFixer(commentType, context, leadingComments, headerLines, eol, numNewlines) {
334
- return function(fixer) {
339
+ function genReplaceFixer(commentType, sourceCode, leadingComments, headerLines, eol, numNewlines) {
340
+ return function (fixer) {
335
341
  const commentRange = genCommentsRange(leadingComments);
336
- const emptyLines = leadingEmptyLines(contextSourceCode(context).text.substring(commentRange[1]));
342
+ const emptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
337
343
  const missingNewlines = Math.max(0, numNewlines - emptyLines);
338
344
  const eols = eol.repeat(missingNewlines);
339
345
  return fixer.replaceTextRange(
@@ -352,7 +358,7 @@ function genReplaceFixer(commentType, context, leadingComments, headerLines, eol
352
358
  * @returns {ReportFixer} The fix to apply.
353
359
  */
354
360
  function genEmptyLinesFixer(leadingComments, eol, missingEmptyLinesCount) {
355
- return function(fixer) {
361
+ return function (fixer) {
356
362
  return fixer.insertTextAfterRange(
357
363
  genCommentsRange(leadingComments),
358
364
  eol.repeat(missingEmptyLinesCount)
@@ -380,17 +386,6 @@ function getEol(style) {
380
386
  }
381
387
  }
382
388
 
383
- /**
384
- * Tests if the first line in the source code (after a Unix she-bang) is a
385
- * comment. Does not tolerate empty lines before the first match.
386
- * @param {string} src Source code to test.
387
- * @returns {boolean} `true` if there is a comment or `false` otherwise.
388
- */
389
- function hasHeader(src) {
390
- const srcWithoutShebang = src.replace(/^#![^\n]*\r?\n/, "");
391
- return srcWithoutShebang.startsWith("/*") || srcWithoutShebang.startsWith("//");
392
- }
393
-
394
389
  /**
395
390
  * Asserts on an expression and adds template texts to the failure message.
396
391
  * Helper to write cleaner code.
@@ -462,14 +457,14 @@ function transformLegacyOptions(originalOptions) {
462
457
  transformedOptions.trailingEmptyLines = { minimum: originalOptions[2] };
463
458
  if (originalOptions.length === 4) {
464
459
  schemaAssert(typeof originalOptions[3] === "object",
465
- "Fourth header option after severity should be either number of required trailing empty lines or " +
466
- "a settings object");
460
+ "Fourth header option after severity should be either number of required trailing empty lines or "
461
+ + "a settings object");
467
462
  Object.assign(transformedOptions, originalOptions[3]);
468
463
  }
469
464
  } else {
470
465
  schemaAssert(typeof originalOptions[2] === "object",
471
- "Third header option after severity should be either number of required trailing empty lines or a " +
472
- "settings object");
466
+ "Third header option after severity should be either number of required trailing empty lines or a "
467
+ + "settings object");
473
468
  Object.assign(transformedOptions, originalOptions[2]);
474
469
  }
475
470
  }
@@ -483,35 +478,23 @@ function transformLegacyOptions(originalOptions) {
483
478
  * else `false`.
484
479
  */
485
480
  function isFileBasedHeaderConfig(config) {
486
- return Object.prototype.hasOwnProperty.call(config, "file");
481
+ return "file" in config;
487
482
  }
488
483
 
489
484
  /**
490
- * Type guard for `InlineConfig`.
491
- * @param {FileBasedConfig | InlineConfig} config The header configuration.
492
- * @returns {asserts config is InlineConfig} Asserts `config` is
493
- * `LineBasedConfig`.
485
+ * Transforms file template-based matching rules to inline rules for further
486
+ * use.
487
+ * @param {FileBasedConfig | InlineConfig} matcher The matching rule.
488
+ * @returns {InlineConfig} The resulting normalized configuration.
494
489
  */
495
- function assertLineBasedHeaderConfig(config) {
496
- assert.ok(Object.prototype.hasOwnProperty.call(config, "lines"));
497
- }
498
-
499
- /**
500
- * Transforms a set of new-style options adding defaults and standardizing on
501
- * one of multiple config styles.
502
- * @param {HeaderOptions} originalOptions New-style options to normalize.
503
- * @returns {HeaderOptions} Normalized options.
504
- */
505
- function normalizeOptions(originalOptions) {
506
- const options = structuredClone(originalOptions);
507
-
508
- if (isFileBasedHeaderConfig(originalOptions.header)) {
509
- const text = fs.readFileSync(originalOptions.header.file, originalOptions.header.encoding || "utf8");
490
+ function normalizeMatchingRules(matcher) {
491
+ if (isFileBasedHeaderConfig(matcher)) {
492
+ const text = fs.readFileSync(matcher.file, matcher.encoding || "utf8");
510
493
  const [commentType, lines] = commentParser(text);
511
- options.header = { commentType, lines };
494
+ return { commentType, lines };
512
495
  }
513
- assertLineBasedHeaderConfig(options.header);
514
- options.header.lines = options.header.lines.flatMap(
496
+ const commentType = matcher.commentType;
497
+ const lines = matcher.lines.flatMap(
515
498
  (line) => {
516
499
  if (typeof line === "string") {
517
500
  return /** @type {HeaderLine[]} */(line.split(/\r?\n/));
@@ -529,11 +512,31 @@ function normalizeOptions(originalOptions) {
529
512
  }
530
513
  return [{ pattern }];
531
514
  });
515
+ return { commentType, lines };
516
+ }
517
+
518
+ /**
519
+ * Transforms a set of new-style options adding defaults and standardizing on
520
+ * one of multiple config styles.
521
+ * @param {HeaderOptions} originalOptions New-style options to normalize.
522
+ * @returns {HeaderOptions} Normalized options.
523
+ */
524
+ function normalizeOptions(originalOptions) {
525
+ const options = structuredClone(originalOptions);
526
+
527
+ options.header = normalizeMatchingRules(originalOptions.header);
532
528
 
533
529
  if (!options.lineEndings) {
534
530
  options.lineEndings = "os";
535
531
  }
536
532
 
533
+ if (originalOptions.leadingComments) {
534
+ options.leadingComments = {
535
+ comments: originalOptions.leadingComments.comments.map((c) => normalizeMatchingRules(c))
536
+ };
537
+ } else {
538
+ options.leadingComments = { comments: [] };
539
+ }
537
540
  if (!options.trailingEmptyLines) {
538
541
  options.trailingEmptyLines = {};
539
542
  }
@@ -558,9 +561,7 @@ function normalizeOptions(originalOptions) {
558
561
  */
559
562
  function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
560
563
  assert.ok(leadingComments);
561
- const loc = leadingComments[leadingComments.length - 1].loc;
562
- assertDefined(loc);
563
- assertNotNull(loc);
564
+ const loc = /** @type {SourceLocation} */ (leadingComments[leadingComments.length - 1].loc);
564
565
  const lastCommentLineLocEnd = loc.end;
565
566
  return {
566
567
  start: lastCommentLineLocEnd,
@@ -571,6 +572,322 @@ function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
571
572
  };
572
573
  }
573
574
 
575
+ /**
576
+ * Matches comments against of header content-matching rules. An object performs
577
+ * a number of expensive operations only once and thus can be used multiple
578
+ * times to test different comments.
579
+ */
580
+ class CommentMatcher {
581
+ /**
582
+ * Initializes the matcher for a specific comment-matching rules.
583
+ * @param {InlineConfig} headerConfig Content-matching rules.
584
+ * @param {string} eol The EOL characters used.
585
+ * @param {number} numLines The requirred minimum number of trailing empty
586
+ * lines.
587
+ */
588
+ constructor(headerConfig, eol, numLines) {
589
+ this.commentType = headerConfig.commentType;
590
+ this.headerLines = headerConfig.lines.map((line) => isPattern(line) ? line.pattern : line);
591
+ this.eol = eol;
592
+ this.numLines = numLines;
593
+ }
594
+
595
+ /**
596
+ * @typedef {ViolationReport<JSSyntaxElement, string>} ViolationReportBad
597
+ * @typedef {ViolationReportBad & { messageId: string }} ViolationReportEx
598
+ */
599
+
600
+ /**
601
+ * Performs a validation of a comment against a header matching
602
+ * configuration.
603
+ * @param {Comment[]} leadingComments The block comment or sequence of line
604
+ * comments to test.
605
+ * @param {SourceCode} sourceCode The source code AST.
606
+ * @returns {ViolationReportEx | null} If set a
607
+ * violation report to pass back to ESLint or interpret as necessary.
608
+ */
609
+ validate(leadingComments, sourceCode) {
610
+
611
+ const firstLeadingCommentLoc = /** @type {SourceLocation} */ (leadingComments[0].loc);
612
+ const firstLeadingCommentRange = /** @type {[number, number]} */ (leadingComments[0].range);
613
+
614
+ const lastLeadingCommentLoc = /** @type {SourceLocation} */ (leadingComments[leadingComments.length - 1].loc);
615
+
616
+ if (leadingComments[0].type.toLowerCase() !== this.commentType) {
617
+ return {
618
+ loc: {
619
+ start: firstLeadingCommentLoc.start,
620
+ end: lastLeadingCommentLoc.end
621
+ },
622
+ messageId: "incorrectCommentType",
623
+ data: {
624
+ commentType: this.commentType
625
+ },
626
+ };
627
+ }
628
+ if (this.commentType === commentTypeOptions.line) {
629
+ if (this.headerLines.length === 1) {
630
+ const leadingCommentValues = leadingComments.map((c) => c.value);
631
+ if (
632
+ !match(leadingCommentValues.join("\n"), this.headerLines[0])
633
+ && !match(leadingCommentValues.join("\r\n"), this.headerLines[0])
634
+ ) {
635
+ return {
636
+ loc: {
637
+ start: firstLeadingCommentLoc.start,
638
+ end: lastLeadingCommentLoc.end
639
+ },
640
+ messageId: "incorrectHeader",
641
+ data: {
642
+ pattern: this.headerLines[0].toString()
643
+ }
644
+ };
645
+ }
646
+ } else {
647
+ for (let i = 0; i < this.headerLines.length; i++) {
648
+ if (leadingComments.length - 1 < i) {
649
+ return {
650
+ loc: {
651
+ start: lastLeadingCommentLoc.end,
652
+ end: lastLeadingCommentLoc.end
653
+ },
654
+ messageId: "headerTooShort",
655
+ data: {
656
+ remainder: this.headerLines.slice(i).join(this.eol)
657
+ },
658
+ };
659
+ }
660
+ const headerLine = this.headerLines[i];
661
+ const comment = leadingComments[i];
662
+ const commentLoc = /** @type {SourceLocation} */ (comment.loc);
663
+ if (typeof headerLine === "string") {
664
+ const leadingCommentLength = comment.value.length;
665
+ const headerLineLength = headerLine.length;
666
+ for (let j = 0; j < Math.min(leadingCommentLength, headerLineLength); j++) {
667
+ if (comment.value[j] !== headerLine[j]) {
668
+ return {
669
+ loc: {
670
+ start: {
671
+ column: "//".length + j,
672
+ line: commentLoc.start.line
673
+ },
674
+ end: commentLoc.end
675
+ },
676
+ messageId: "headerLineMismatchAtPos",
677
+ data: {
678
+ expected: headerLine.substring(j)
679
+ },
680
+ };
681
+ }
682
+ }
683
+ if (leadingCommentLength < headerLineLength) {
684
+ return {
685
+ loc: {
686
+ start: commentLoc.end,
687
+ end: commentLoc.end,
688
+ },
689
+ messageId: "headerLineTooShort",
690
+ data: {
691
+ remainder: headerLine.substring(leadingCommentLength)
692
+ },
693
+ };
694
+ }
695
+ if (leadingCommentLength > headerLineLength) {
696
+ return {
697
+ loc: {
698
+ start: {
699
+ column: "//".length + headerLineLength,
700
+ line: commentLoc.start.line
701
+ },
702
+ end: commentLoc.end,
703
+ },
704
+ messageId: "headerLineTooLong",
705
+ };
706
+ }
707
+ } else {
708
+ if (!match(comment.value, headerLine)) {
709
+ return {
710
+ loc: {
711
+ start: {
712
+ column: "//".length,
713
+ line: commentLoc.start.line,
714
+ },
715
+ end: commentLoc.end,
716
+ },
717
+ messageId: "incorrectHeaderLine",
718
+ data: {
719
+ pattern: headerLine.toString()
720
+ },
721
+ };
722
+ }
723
+ }
724
+ }
725
+ }
726
+
727
+ const commentRange = /** @type {[number, number]} */ (leadingComments[this.headerLines.length - 1].range);
728
+ const actualLeadingEmptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
729
+ const missingEmptyLines = this.numLines - actualLeadingEmptyLines;
730
+ if (missingEmptyLines > 0) {
731
+ return {
732
+ loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
733
+ messageId: "noNewlineAfterHeader",
734
+ data: {
735
+ expected: this.numLines,
736
+ actual: actualLeadingEmptyLines
737
+ },
738
+ };
739
+ }
740
+
741
+ return null;
742
+ }
743
+ // if block comment pattern has more than 1 line, we also split the
744
+ // comment
745
+ let leadingLines = [leadingComments[0].value];
746
+ if (this.headerLines.length > 1) {
747
+ leadingLines = leadingComments[0].value.split(/\r?\n/);
748
+ }
749
+
750
+ /** @type {null | string} */
751
+ let errorMessageId = null;
752
+ /** @type {undefined | Record<string, string>} */
753
+ let errorMessageData;
754
+ /** @type {null | SourceLocation} */
755
+ let errorMessageLoc = null;
756
+ for (let i = 0; i < this.headerLines.length; i++) {
757
+ if (leadingLines.length - 1 < i) {
758
+ return {
759
+ loc: {
760
+ start: lastLeadingCommentLoc.end,
761
+ end: lastLeadingCommentLoc.end
762
+ },
763
+ messageId: "headerTooShort",
764
+ data: {
765
+ remainder: this.headerLines.slice(i).join(this.eol)
766
+ },
767
+ };
768
+ }
769
+ const leadingLine = leadingLines[i];
770
+ const headerLine = this.headerLines[i];
771
+ if (typeof headerLine === "string") {
772
+ for (let j = 0; j < Math.min(leadingLine.length, headerLine.length); j++) {
773
+ if (leadingLine[j] !== headerLine[j]) {
774
+ errorMessageId = "headerLineMismatchAtPos";
775
+ const columnOffset = i === 0 ? "/*".length : 0;
776
+ const line = firstLeadingCommentLoc.start.line + i;
777
+ errorMessageLoc = {
778
+ start: {
779
+ column: columnOffset + j,
780
+ line
781
+ },
782
+ end: {
783
+ column: columnOffset + leadingLine.length,
784
+ line
785
+ }
786
+ };
787
+ errorMessageData = {
788
+ expected: headerLine.substring(j)
789
+ };
790
+ break;
791
+ }
792
+ }
793
+ if (errorMessageId) {
794
+ break;
795
+ }
796
+ if (leadingLine.length < headerLine.length) {
797
+ errorMessageId = "headerLineTooShort";
798
+ const startColumn = (i === 0 ? "/*".length : 0) + leadingLine.length;
799
+ errorMessageLoc = {
800
+ start: {
801
+ column: startColumn,
802
+ line: firstLeadingCommentLoc.start.line + i
803
+ },
804
+ end: {
805
+ column: startColumn + 1,
806
+ line: firstLeadingCommentLoc.start.line + i
807
+ }
808
+ };
809
+ errorMessageData = {
810
+ remainder: headerLine.substring(leadingLine.length)
811
+ };
812
+ break;
813
+ }
814
+ if (leadingLine.length > headerLine.length) {
815
+ errorMessageId = "headerLineTooLong";
816
+ errorMessageLoc = {
817
+ start: {
818
+ column: (i === 0 ? "/*".length : 0) + headerLine.length,
819
+ line: firstLeadingCommentLoc.start.line + i
820
+ },
821
+ end: {
822
+ column: (i === 0 ? "/*".length : 0) + leadingLine.length,
823
+ line: firstLeadingCommentLoc.start.line + i
824
+ }
825
+ };
826
+ break;
827
+ }
828
+ } else {
829
+ if (!match(leadingLine, headerLine)) {
830
+ errorMessageId = "incorrectHeaderLine";
831
+ errorMessageData = {
832
+ pattern: headerLine.toString()
833
+ };
834
+ const columnOffset = i === 0 ? "/*".length : 0;
835
+ errorMessageLoc = {
836
+ start: {
837
+ column: columnOffset + 0,
838
+ line: firstLeadingCommentLoc.start.line + i
839
+ },
840
+ end: {
841
+ column: columnOffset + leadingLine.length,
842
+ line: firstLeadingCommentLoc.start.line + i
843
+ }
844
+ };
845
+ break;
846
+ }
847
+ }
848
+ }
849
+
850
+ if (!errorMessageId && leadingLines.length > this.headerLines.length) {
851
+ errorMessageId = "headerTooLong";
852
+ errorMessageLoc = {
853
+ start: {
854
+ column: (this.headerLines.length === 0 ? "/*".length : 0) + 0,
855
+ line: firstLeadingCommentLoc.start.line + this.headerLines.length
856
+ },
857
+ end: {
858
+ column: lastLeadingCommentLoc.end.column - "*/".length,
859
+ line: lastLeadingCommentLoc.end.line
860
+ }
861
+ };
862
+ }
863
+
864
+ if (errorMessageId) {
865
+ return {
866
+ loc: /** @type {SourceLocation} */ (errorMessageLoc),
867
+ messageId: errorMessageId,
868
+ data: errorMessageData,
869
+ };
870
+ }
871
+
872
+ const actualLeadingEmptyLines =
873
+ leadingEmptyLines(sourceCode.text.substring(firstLeadingCommentRange[1]));
874
+ const missingEmptyLines = this.numLines - actualLeadingEmptyLines;
875
+ if (missingEmptyLines > 0) {
876
+ return {
877
+ loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
878
+ messageId: "noNewlineAfterHeader",
879
+ data: {
880
+ expected: this.numLines,
881
+ actual: actualLeadingEmptyLines
882
+ },
883
+ };
884
+ }
885
+
886
+ return null;
887
+ }
888
+ }
889
+
890
+
574
891
  /** @type {Rule.RuleModule} */
575
892
  const headerRule = {
576
893
  meta: {
@@ -591,14 +908,33 @@ const headerRule = {
591
908
  }
592
909
  ],
593
910
  messages: {
594
- headerLineMismatchAtPos: "header line does not match expected after this position; expected: {{expected}}",
911
+ // messages customized for header validation.
912
+ headerLineMismatchAtPos:
913
+ "header line does not match expected after this position; expected: '{{expected}}'",
595
914
  headerLineTooLong: "header line longer than expected",
596
- headerLineTooShort: "header line shorter than expected; missing: {{remainder}}",
597
- headerTooShort: "header too short: missing lines: {{remainder}}",
915
+ headerLineTooShort: "header line shorter than expected; missing: '{{remainder}}'",
916
+ headerTooShort: "header too short; missing lines: '{{remainder}}'",
598
917
  headerTooLong: "header too long",
599
918
  incorrectCommentType: "header should be a {{commentType}} comment",
600
- incorrectHeader: "incorrect header",
601
- incorrectHeaderLine: "header line does not match pattern: {{pattern}}",
919
+ incorrectHeader: "header does not match pattern: '{{pattern}}'",
920
+ incorrectHeaderLine: "header line does not match pattern: '{{pattern}}'",
921
+ // messages customized for leading comments validation.
922
+ "leadingComment-headerLineMismatchAtPos":
923
+ "leading comment validation failed: line does not match expected after this position; "
924
+ + "expected: '{{expected}}'",
925
+ "leadingComment-headerLineTooLong": "leading comment validation failed: line longer than expected",
926
+ "leadingComment-headerLineTooShort":
927
+ "leading comment validation failed: line shorter than expected; missing: '{{remainder}}'",
928
+ "leadingComment-headerTooShort":
929
+ "leading comment validation failed: comment too short; missing lines: '{{remainder}}'",
930
+ "leadingComment-headerTooLong": "leading comment validation failed: comment too long",
931
+ "leadingComment-incorrectCommentType":
932
+ "leading comment validation failed: should be a {{commentType}} comment",
933
+ "leadingComment-incorrectHeader":
934
+ "leading comment validation failed: comment does not match pattern: '{{pattern}}'",
935
+ "leadingComment-incorrectHeaderLine":
936
+ "leading comment validation failed: comment line does not match pattern: '{{pattern}}'",
937
+ // messages only applicable to header validation.
602
938
  missingHeader: "missing header",
603
939
  noNewlineAfterHeader: "not enough newlines after header: expected: {{expected}}, actual: {{actual}}"
604
940
  }
@@ -609,42 +945,24 @@ const headerRule = {
609
945
  * @param {RuleContext} context ESLint rule execution context.
610
946
  * @returns {NodeListener} The rule definition.
611
947
  */
612
- create: function(context) {
948
+ create: function (context) {
613
949
 
614
- const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */ (context.options));
950
+ const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */(context.options));
615
951
  const options = normalizeOptions(newStyleOptions);
616
-
617
- assertLineBasedHeaderConfig(options.header);
618
- const commentType = /** @type {CommentType} */ (options.header.commentType);
619
-
620
- const eol = getEol(
621
- /** @type {LineEndingOption} */ (options.lineEndings)
622
- );
623
-
624
- /** @type {string[]} */
625
- let fixLines = [];
626
- // If any of the lines are regular expressions, then we can't
627
- // automatically fix them. We set this to true below once we
628
- // ensure none of the lines are of type RegExp
629
- let canFix = true;
630
- const headerLines = options.header.lines.map(function(line) {
631
- // Can only fix regex option if a template is also provided
952
+ const eol = getEol(/** @type {LineEndingOption} */(options.lineEndings));
953
+ const header = /** @type {InlineConfig} */ (options.header);
954
+ const canFix = !header.lines.some((line) => isPattern(line) && !("template" in line));
955
+ const fixLines = header.lines.map((line) => {
632
956
  if (isPattern(line)) {
633
- if (Object.prototype.hasOwnProperty.call(line, "template")) {
634
- fixLines.push(/** @type {string} */ (line.template));
635
- } else {
636
- canFix = false;
637
- fixLines.push("");
638
- }
639
- return line.pattern;
640
- } else {
641
- fixLines.push(/** @type {string} */ (line));
642
- return line;
957
+ return ("template" in line) ? /** @type {string} */(line.template) : "";
643
958
  }
959
+ return /** @type {string} */(line);
644
960
  });
645
-
646
-
647
961
  const numLines = /** @type {number} */ (options.trailingEmptyLines?.minimum);
962
+ const headerMatcher = new CommentMatcher(header, eol, numLines);
963
+ const allowedLeadingComments = /** @type {LeadingComments} */ (options.leadingComments).comments;
964
+ const allowedCommentsMatchers =
965
+ allowedLeadingComments.map((c) => new CommentMatcher(/** @type {InlineConfig} */(c), eol, 0));
648
966
 
649
967
  return {
650
968
  /**
@@ -652,58 +970,36 @@ const headerRule = {
652
970
  * header validation.
653
971
  * @returns {void}
654
972
  */
655
- Program: function() {
973
+ Program: function () {
656
974
  const sourceCode = contextSourceCode(context);
657
- if (!hasHeader(sourceCode.text)) {
658
- const hasShebang = sourceCode.text.startsWith("#!");
659
- const line = hasShebang ? 2 : 1;
975
+ const leadingComments = getLeadingComments(sourceCode);
976
+ const hasShebang = leadingComments.length > 0
977
+ && /** @type {string} */ (leadingComments[0][0].type) === "Shebang";
978
+ let startingHeaderLine = 1;
979
+ if (hasShebang) {
980
+ leadingComments.splice(0, 1);
981
+ startingHeaderLine = 2;
982
+ }
983
+
984
+ if (leadingComments.length === 0
985
+ || /** @type {SourceLocation} */ (leadingComments[0][0].loc).start.line > startingHeaderLine) {
986
+
660
987
  context.report({
661
988
  loc: {
662
989
  start: {
663
- column: 1,
664
- line
990
+ column: 0,
991
+ line: startingHeaderLine
665
992
  },
666
993
  end: {
667
- column: 1,
668
- line
994
+ column: 0,
995
+ line: startingHeaderLine
669
996
  }
670
997
  },
671
998
  messageId: "missingHeader",
672
- fix: genPrependFixer(
673
- commentType,
674
- context,
675
- fixLines,
676
- eol,
677
- numLines)
678
- });
679
- return;
680
- }
681
- const leadingComments = getLeadingComments(context);
682
- const firstLeadingCommentLoc = leadingComments[0].loc;
683
- const firstLeadingCommentRange = leadingComments[0].range;
684
- assertDefined(firstLeadingCommentRange);
685
-
686
- const lastLeadingCommentLoc = leadingComments[leadingComments.length - 1].loc;
687
-
688
- if (leadingComments[0].type.toLowerCase() !== commentType) {
689
- assertDefined(firstLeadingCommentLoc);
690
- assertNotNull(firstLeadingCommentLoc);
691
- assertDefined(lastLeadingCommentLoc);
692
- assertNotNull(lastLeadingCommentLoc);
693
- context.report({
694
- loc: {
695
- start: firstLeadingCommentLoc.start,
696
- end: lastLeadingCommentLoc.end
697
- },
698
- messageId: "incorrectCommentType",
699
- data: {
700
- commentType: commentType
701
- },
702
999
  fix: canFix
703
- ? genReplaceFixer(
704
- commentType,
705
- context,
706
- leadingComments,
1000
+ ? genPrependFixer(
1001
+ headerMatcher.commentType,
1002
+ sourceCode,
707
1003
  fixLines,
708
1004
  eol,
709
1005
  numLines)
@@ -711,366 +1007,69 @@ const headerRule = {
711
1007
  });
712
1008
  return;
713
1009
  }
714
- if (commentType === commentTypeOptions.line) {
715
- if (headerLines.length === 1) {
716
- const leadingCommentValues = leadingComments.map((c) => c.value);
717
- if (
718
- !match(leadingCommentValues.join("\n"), headerLines[0])
719
- && !match(leadingCommentValues.join("\r\n"), headerLines[0])
720
- ) {
721
- assertDefined(firstLeadingCommentLoc);
722
- assertNotNull(firstLeadingCommentLoc);
723
- assertDefined(lastLeadingCommentLoc);
724
- assertNotNull(lastLeadingCommentLoc);
725
- context.report({
726
- loc: {
727
- start: firstLeadingCommentLoc.start,
728
- end: lastLeadingCommentLoc.end
729
- },
730
- messageId: "incorrectHeader",
731
- fix: canFix
732
- ? genReplaceFixer(
733
- commentType,
734
- context,
735
- leadingComments,
736
- fixLines,
737
- eol,
738
- numLines)
739
- : null
740
- });
741
- return;
742
- }
743
- } else {
744
- for (let i = 0; i < headerLines.length; i++) {
745
- if (leadingComments.length - 1 < i) {
746
- assertDefined(lastLeadingCommentLoc);
747
- assertNotNull(lastLeadingCommentLoc);
748
- context.report({
749
- loc: {
750
- start: lastLeadingCommentLoc.end,
751
- end: lastLeadingCommentLoc.end
752
- },
753
- messageId: "headerTooShort",
754
- data: {
755
- remainder: headerLines.slice(i).join(eol)
756
- },
757
- fix: canFix
758
- ? genReplaceFixer(
759
- commentType,
760
- context,
761
- leadingComments,
762
- fixLines,
763
- eol,
764
- numLines)
765
- : null
766
- });
767
- return;
768
- }
769
- const headerLine = headerLines[i];
770
- const comment = leadingComments[i];
771
- const commentLoc = comment.loc;
772
- assertDefined(commentLoc);
773
- assertNotNull(commentLoc);
774
- if (typeof headerLine === "string") {
775
- const leadingCommentLength = comment.value.length;
776
- const headerLineLength = headerLine.length;
777
- for (let j = 0; j < Math.min(leadingCommentLength, headerLineLength); j++) {
778
- if (comment.value[j] !== headerLine[j]) {
779
- context.report({
780
- loc: {
781
- start: {
782
- column: "//".length + j,
783
- line: commentLoc.start.line
784
- },
785
- end: commentLoc.end
786
- },
787
- messageId: "headerLineMismatchAtPos",
788
- data: {
789
- expected: headerLine.substring(j)
790
- },
791
- fix: genReplaceFixer(
792
- commentType,
793
- context,
794
- leadingComments,
795
- fixLines,
796
- eol,
797
- numLines)
798
- });
799
- return;
800
- }
801
- }
802
- if (leadingCommentLength < headerLineLength) {
803
- context.report({
804
- loc: {
805
- start: commentLoc.end,
806
- end: commentLoc.end,
807
- },
808
- messageId: "headerLineTooShort",
809
- data: {
810
- remainder: headerLine.substring(leadingCommentLength)
811
- },
812
- fix: canFix
813
- ? genReplaceFixer(
814
- commentType,
815
- context,
816
- leadingComments,
817
- fixLines,
818
- eol,
819
- numLines)
820
- : null
821
- });
822
- return;
823
- }
824
- if (leadingCommentLength > headerLineLength) {
825
- context.report({
826
- loc: {
827
- start: {
828
- column: "//".length + headerLineLength,
829
- line: commentLoc.start.line
830
- },
831
- end: commentLoc.end,
832
- },
833
- messageId: "headerLineTooLong",
834
- fix: canFix
835
- ? genReplaceFixer(
836
- commentType,
837
- context,
838
- leadingComments,
839
- fixLines,
840
- eol,
841
- numLines)
842
- : null
843
- });
844
- return;
845
- }
846
- } else {
847
- if (!match(comment.value, headerLine)) {
848
- context.report({
849
- loc: {
850
- start: {
851
- column: "//".length,
852
- line: commentLoc.start.line,
853
- },
854
- end: commentLoc.end,
855
- },
856
- messageId: "incorrectHeaderLine",
857
- data: {
858
- pattern: headerLine.toString()
859
- },
860
- fix: canFix
861
- ? genReplaceFixer(
862
- commentType,
863
- context,
864
- leadingComments,
865
- fixLines,
866
- eol,
867
- numLines)
868
- : null
869
- });
870
- return;
871
- }
872
- }
873
- }
874
- }
875
1010
 
876
- const commentRange = leadingComments[headerLines.length - 1].range;
877
- assertDefined(commentRange);
878
- const actualLeadingEmptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
879
- const missingEmptyLines = numLines - actualLeadingEmptyLines;
880
- if (missingEmptyLines > 0) {
881
- context.report({
882
- loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
883
- messageId: "noNewlineAfterHeader",
884
- data: {
885
- expected: numLines,
886
- actual: actualLeadingEmptyLines
887
- },
888
- fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
889
- });
890
- }
891
- return;
892
- }
893
- // if block comment pattern has more than 1 line, we also split
894
- // the comment
895
- let leadingLines = [leadingComments[0].value];
896
- if (headerLines.length > 1) {
897
- leadingLines = leadingComments[0].value.split(/\r?\n/);
898
- }
1011
+ for (const leadingComment of leadingComments) {
899
1012
 
900
- /** @type {null | string} */
901
- let errorMessageId = null;
902
- /** @type {undefined | Record<string, string>} */
903
- let errorMessageData;
904
- /** @type {null | SourceLocation} */
905
- let errorMessageLoc = null;
906
- for (let i = 0; i < headerLines.length; i++) {
907
- if (leadingLines.length - 1 < i) {
908
- assertDefined(lastLeadingCommentLoc);
909
- assertNotNull(lastLeadingCommentLoc);
910
- context.report({
911
- loc: {
912
- start: lastLeadingCommentLoc.end,
913
- end: lastLeadingCommentLoc.end
914
- },
915
- messageId: "headerTooShort",
916
- data: {
917
- remainder: headerLines.slice(i).join(eol)
918
- },
919
- fix: canFix
920
- ? genReplaceFixer(
921
- commentType,
922
- context,
923
- leadingComments,
924
- fixLines,
925
- eol,
926
- numLines)
927
- : null
928
- });
1013
+ const headerReport = headerMatcher.validate(leadingComment, sourceCode);
1014
+ if (headerReport === null) {
929
1015
  return;
930
1016
  }
931
- const leadingLine = leadingLines[i];
932
- const headerLine = headerLines[i];
933
- if (typeof headerLine === "string") {
934
- for (let j = 0; j < Math.min(leadingLine.length, headerLine.length); j++) {
935
- if (leadingLine[j] !== headerLine[j]) {
936
- errorMessageId = "headerLineMismatchAtPos";
937
- const columnOffset = i === 0 ? "/*".length : 0;
938
- assertDefined(firstLeadingCommentLoc);
939
- assertNotNull(firstLeadingCommentLoc);
940
- const line = firstLeadingCommentLoc.start.line + i;
941
- errorMessageLoc = {
942
- start: {
943
- column: columnOffset + j,
944
- line
945
- },
946
- end: {
947
- column: columnOffset + leadingLine.length,
948
- line
949
- }
950
- };
951
- errorMessageData = {
952
- expected: headerLine.substring(j)
953
- };
954
- break;
955
- }
956
- }
957
- if (errorMessageId) {
958
- break;
959
- }
960
- if (leadingLine.length < headerLine.length) {
961
- errorMessageId = "headerLineTooShort";
962
- const startColumn = (i === 0 ? "/*".length : 0) + leadingLine.length;
963
- assertDefined(firstLeadingCommentLoc);
964
- assertNotNull(firstLeadingCommentLoc);
965
- errorMessageLoc = {
966
- start: {
967
- column: startColumn,
968
- line: firstLeadingCommentLoc.start.line + i
969
- },
970
- end: {
971
- column: startColumn + 1,
972
- line: firstLeadingCommentLoc.start.line + i
973
- }
974
- };
975
- errorMessageData = {
976
- remainder: headerLine.substring(leadingLine.length)
977
- };
978
- break;
979
- }
980
- if (leadingLine.length > headerLine.length) {
981
- assertDefined(firstLeadingCommentLoc);
982
- assertNotNull(firstLeadingCommentLoc);
983
- errorMessageId = "headerLineTooLong";
984
- errorMessageLoc = {
985
- start: {
986
- column: (i === 0 ? "/*".length : 0) + headerLine.length,
987
- line: firstLeadingCommentLoc.start.line + i
988
- },
989
- end: {
990
- column: (i === 0 ? "/*".length : 0) + leadingLine.length,
991
- line: firstLeadingCommentLoc.start.line + i
992
- }
993
- };
994
- break;
1017
+ const leadingCommentReports =
1018
+ allowedCommentsMatchers.map((m) => m.validate(leadingComment, sourceCode));
1019
+ const commentMatched = leadingCommentReports.some((report) => report === null);
1020
+
1021
+ if (!commentMatched) {
1022
+ if ("messageId" in headerReport && headerReport.messageId === "noNewlineAfterHeader") {
1023
+ const { expected, actual } =
1024
+ /** @type {{ expected: number, actual: number }} */ (headerReport.data);
1025
+ headerReport.fix = genEmptyLinesFixer(leadingComment, eol, expected - actual);
1026
+ } else if (canFix) {
1027
+ headerReport.fix = genReplaceFixer(
1028
+ headerMatcher.commentType,
1029
+ sourceCode,
1030
+ leadingComment,
1031
+ fixLines,
1032
+ eol,
1033
+ numLines);
995
1034
  }
996
- } else {
997
- if (!match(leadingLine, headerLine)) {
998
- errorMessageId = "incorrectHeaderLine";
999
- errorMessageData = {
1000
- pattern: headerLine.toString()
1001
- };
1002
- const columnOffset = i === 0 ? "/*".length : 0;
1003
- assertDefined(firstLeadingCommentLoc);
1004
- assertNotNull(firstLeadingCommentLoc);
1005
- errorMessageLoc = {
1006
- start: {
1007
- column: columnOffset + 0,
1008
- line: firstLeadingCommentLoc.start.line + i
1009
- },
1010
- end: {
1011
- column: columnOffset + leadingLine.length,
1012
- line: firstLeadingCommentLoc.start.line + i
1013
- }
1014
- };
1015
- break;
1035
+
1036
+ context.report(headerReport);
1037
+ for (const commentReport of leadingCommentReports) {
1038
+ if (commentReport !== null) {
1039
+ /** @type {{ messageId: string }} */ (commentReport).messageId =
1040
+ "leadingComment-" + /** @type {{ messageId: string }} */ (commentReport).messageId;
1041
+ context.report(commentReport);
1042
+ }
1016
1043
  }
1044
+ return;
1017
1045
  }
1018
1046
  }
1019
1047
 
1020
- if (!errorMessageId && leadingLines.length > headerLines.length) {
1021
- errorMessageId = "headerTooLong";
1022
- assertDefined(firstLeadingCommentLoc);
1023
- assertNotNull(firstLeadingCommentLoc);
1024
- assertDefined(lastLeadingCommentLoc);
1025
- assertNotNull(lastLeadingCommentLoc);
1026
- errorMessageLoc = {
1048
+ const lastComment = leadingComments[leadingComments.length - 1];
1049
+ const lastCommentLine = lastComment[lastComment.length - 1];
1050
+ const lineIndex = /** @type {number} */ (lastCommentLine.loc?.end.line) + 1;
1051
+
1052
+ context.report({
1053
+ loc: {
1027
1054
  start: {
1028
- column: (headerLines.length === 0 ? "/*".length : 0) + 0,
1029
- line: firstLeadingCommentLoc.start.line + headerLines.length
1055
+ column: 0,
1056
+ line: lineIndex
1030
1057
  },
1031
1058
  end: {
1032
- column: lastLeadingCommentLoc.end.column - "*/".length,
1033
- line: lastLeadingCommentLoc.end.line
1059
+ column: 0,
1060
+ line: lineIndex
1034
1061
  }
1035
- };
1036
- }
1037
-
1038
- if (errorMessageId) {
1039
- if (canFix && headerLines.length > 1) {
1040
- fixLines = [fixLines.join(eol)];
1041
- }
1042
- assertNotNull(errorMessageLoc);
1043
- context.report({
1044
- loc: errorMessageLoc,
1045
- messageId: errorMessageId,
1046
- data: errorMessageData,
1047
- fix: canFix
1048
- ? genReplaceFixer(
1049
- commentType,
1050
- context,
1051
- leadingComments,
1052
- fixLines,
1053
- eol,
1054
- numLines)
1055
- : null
1056
- });
1057
- return;
1058
- }
1059
-
1060
- const actualLeadingEmptyLines =
1061
- leadingEmptyLines(sourceCode.text.substring(firstLeadingCommentRange[1]));
1062
- const missingEmptyLines = numLines - actualLeadingEmptyLines;
1063
- if (missingEmptyLines > 0) {
1064
- context.report({
1065
- loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
1066
- messageId: "noNewlineAfterHeader",
1067
- data: {
1068
- expected: numLines,
1069
- actual: actualLeadingEmptyLines
1070
- },
1071
- fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
1072
- });
1073
- }
1062
+ },
1063
+ messageId: "missingHeader",
1064
+ fix: canFix
1065
+ ? genPrependFixer(
1066
+ headerMatcher.commentType,
1067
+ sourceCode,
1068
+ fixLines,
1069
+ eol,
1070
+ numLines)
1071
+ : null
1072
+ });
1074
1073
  }
1075
1074
  };
1076
1075
  }