@tony.ganchev/eslint-plugin-header 3.2.5 → 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.
@@ -36,7 +36,7 @@ module.exports = {
36
36
  * @param {RuleContext} context ESLint execution context.
37
37
  * @returns {SourceCode} The source-code object.
38
38
  */
39
- contextSourceCode: function(context) {
39
+ contextSourceCode: function (context) {
40
40
  return context.sourceCode
41
41
  || /** @type {RuleContext & { getSourceCode: () => SourceCode }} */(context).getSourceCode();
42
42
  }
@@ -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
@@ -175,53 +176,28 @@ function match(actual, expected) {
175
176
  */
176
177
  function excludeShebangs(comments) {
177
178
  /** @type {Comment[]} */
178
- return comments.filter(function(comment) {
179
+ return comments.filter(function (comment) {
179
180
  return comment.type !== "Shebang";
180
181
  });
181
182
  }
182
183
 
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
- }
192
-
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
- }
202
-
203
184
  /**
204
185
  * Returns either the first block comment or the first set of line comments that
205
186
  * are ONLY separated by a single newline. Note that this does not actually
206
187
  * check if they are at the start of the file since that is already checked by
207
188
  * `hasHeader()`.
208
- * @param {RuleContext} context ESLint execution environment.
189
+ * @param {SourceCode} sourceCode AST.
209
190
  * @returns {Comment[]} Lines That constitute the leading comment.
210
191
  */
211
- function getLeadingComments(context) {
212
- const sourceCode = contextSourceCode(context);
192
+ function getLeadingComments(sourceCode) {
213
193
  const all = excludeShebangs(sourceCode.getAllComments());
214
- assert.ok(all);
215
- assert.ok(all.length);
216
194
  if (all[0].type.toLowerCase() === commentTypeOptions.block) {
217
195
  return [all[0]];
218
196
  }
219
197
  let i = 1;
220
198
  for (; i < all.length; ++i) {
221
- const previousRange = all[i - 1].range;
222
- assertDefined(previousRange);
223
- const currentRange = all[i].range;
224
- assertDefined(currentRange);
199
+ const previousRange = /** @type {[number, number]} */ (all[i - 1].range);
200
+ const currentRange = /** @type {[number, number]} */ (all[i].range);
225
201
  const txt = sourceCode.text.slice(previousRange[1], currentRange[0]);
226
202
  if (!txt.match(/^(\r\n|\r|\n)$/)) {
227
203
  break;
@@ -257,14 +233,10 @@ function genCommentBody(commentType, textArray, eol) {
257
233
  function genCommentsRange(comments) {
258
234
  assert.ok(comments.length);
259
235
  const firstComment = comments[0];
260
- assertDefined(firstComment);
261
- const firstCommentRange = firstComment.range;
262
- assertDefined(firstCommentRange);
236
+ const firstCommentRange = /** @type {[number, number]} */ (firstComment.range);
263
237
  const start = firstCommentRange[0];
264
238
  const lastComment = comments.slice(-1)[0];
265
- assertDefined(lastComment);
266
- const lastCommentRange = lastComment.range;
267
- assertDefined(lastCommentRange);
239
+ const lastCommentRange = /** @type {[number, number]} */ (lastComment.range);
268
240
  const end = lastCommentRange[1];
269
241
  return [start, end];
270
242
  }
@@ -292,17 +264,16 @@ function leadingEmptyLines(src) {
292
264
  /**
293
265
  * Factory for fixer that adds a missing header.
294
266
  * @param {CommentType} commentType Type of comment to use.
295
- * @param {RuleContext} context ESLint execution runtime.
267
+ * @param {SourceCode} sourceCode AST.
296
268
  * @param {string[]} headerLines Lines of the header comment.
297
269
  * @param {LineEnding} eol End-of-line characters.
298
270
  * @param {number} numNewlines Number of trailing lines after the comment.
299
271
  * @returns {ReportFixer} The fix to apply.
300
272
  */
301
- function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
302
- return function(fixer) {
273
+ function genPrependFixer(commentType, sourceCode, headerLines, eol, numNewlines) {
274
+ return function (fixer) {
303
275
  let insertPos = 0;
304
276
  let newHeader = genCommentBody(commentType, headerLines, eol);
305
- const sourceCode = contextSourceCode(context);
306
277
  if (sourceCode.text.startsWith("#!")) {
307
278
  const firstNewLinePos = sourceCode.text.indexOf("\n");
308
279
  insertPos = firstNewLinePos === -1 ? sourceCode.text.length : firstNewLinePos + 1;
@@ -323,17 +294,17 @@ function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
323
294
  /**
324
295
  * Factory for fixer that replaces an incorrect header.
325
296
  * @param {CommentType} commentType Type of comment to use.
326
- * @param {RuleContext} context ESLint execution context.
297
+ * @param {SourceCode} sourceCode AST.
327
298
  * @param {Comment[]} leadingComments Comment elements to replace.
328
299
  * @param {string[]} headerLines Lines of the header comment.
329
300
  * @param {LineEnding} eol End-of-line characters.
330
301
  * @param {number} numNewlines Number of trailing lines after the comment.
331
302
  * @returns {ReportFixer} The fix to apply.
332
303
  */
333
- function genReplaceFixer(commentType, context, leadingComments, headerLines, eol, numNewlines) {
334
- return function(fixer) {
304
+ function genReplaceFixer(commentType, sourceCode, leadingComments, headerLines, eol, numNewlines) {
305
+ return function (fixer) {
335
306
  const commentRange = genCommentsRange(leadingComments);
336
- const emptyLines = leadingEmptyLines(contextSourceCode(context).text.substring(commentRange[1]));
307
+ const emptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
337
308
  const missingNewlines = Math.max(0, numNewlines - emptyLines);
338
309
  const eols = eol.repeat(missingNewlines);
339
310
  return fixer.replaceTextRange(
@@ -352,7 +323,7 @@ function genReplaceFixer(commentType, context, leadingComments, headerLines, eol
352
323
  * @returns {ReportFixer} The fix to apply.
353
324
  */
354
325
  function genEmptyLinesFixer(leadingComments, eol, missingEmptyLinesCount) {
355
- return function(fixer) {
326
+ return function (fixer) {
356
327
  return fixer.insertTextAfterRange(
357
328
  genCommentsRange(leadingComments),
358
329
  eol.repeat(missingEmptyLinesCount)
@@ -487,31 +458,19 @@ function isFileBasedHeaderConfig(config) {
487
458
  }
488
459
 
489
460
  /**
490
- * Type guard for `InlineConfig`.
491
- * @param {FileBasedConfig | InlineConfig} config The header configuration.
492
- * @returns {asserts config is InlineConfig} Asserts `config` is
493
- * `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.
494
465
  */
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");
466
+ function normalizeMatchingRules(matcher) {
467
+ if (isFileBasedHeaderConfig(matcher)) {
468
+ const text = fs.readFileSync(matcher.file, matcher.encoding || "utf8");
510
469
  const [commentType, lines] = commentParser(text);
511
- options.header = { commentType, lines };
470
+ return { commentType, lines };
512
471
  }
513
- assertLineBasedHeaderConfig(options.header);
514
- options.header.lines = options.header.lines.flatMap(
472
+ const commentType = matcher.commentType;
473
+ const lines = matcher.lines.flatMap(
515
474
  (line) => {
516
475
  if (typeof line === "string") {
517
476
  return /** @type {HeaderLine[]} */(line.split(/\r?\n/));
@@ -529,6 +488,19 @@ function normalizeOptions(originalOptions) {
529
488
  }
530
489
  return [{ pattern }];
531
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);
532
504
 
533
505
  if (!options.lineEndings) {
534
506
  options.lineEndings = "os";
@@ -558,9 +530,7 @@ function normalizeOptions(originalOptions) {
558
530
  */
559
531
  function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
560
532
  assert.ok(leadingComments);
561
- const loc = leadingComments[leadingComments.length - 1].loc;
562
- assertDefined(loc);
563
- assertNotNull(loc);
533
+ const loc = /** @type {SourceLocation} */ (leadingComments[leadingComments.length - 1].loc);
564
534
  const lastCommentLineLocEnd = loc.end;
565
535
  return {
566
536
  start: lastCommentLineLocEnd,
@@ -571,6 +541,314 @@ function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
571
541
  };
572
542
  }
573
543
 
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
+
574
852
  /** @type {Rule.RuleModule} */
575
853
  const headerRule = {
576
854
  meta: {
@@ -609,50 +887,37 @@ const headerRule = {
609
887
  * @param {RuleContext} context ESLint rule execution context.
610
888
  * @returns {NodeListener} The rule definition.
611
889
  */
612
- create: function(context) {
890
+ create: function (context) {
613
891
 
614
- const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */ (context.options));
892
+ const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */(context.options));
615
893
  const options = normalizeOptions(newStyleOptions);
616
894
 
617
- assertLineBasedHeaderConfig(options.header);
618
- const commentType = /** @type {CommentType} */ (options.header.commentType);
619
-
620
895
  const eol = getEol(
621
- /** @type {LineEndingOption} */ (options.lineEndings)
896
+ /** @type {LineEndingOption} */(options.lineEndings)
622
897
  );
623
898
 
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
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) => {
632
904
  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;
905
+ return ("template" in line) ? /** @type {string} */(line.template) : "";
643
906
  }
907
+ return /** @type {string} */(line);
644
908
  });
645
909
 
646
-
647
910
  const numLines = /** @type {number} */ (options.trailingEmptyLines?.minimum);
648
911
 
912
+ const headerMatcher = new CommentMatcher(header, eol, numLines);
913
+
649
914
  return {
650
915
  /**
651
916
  * Hooks into the processing of the overall script node to do the
652
917
  * header validation.
653
918
  * @returns {void}
654
919
  */
655
- Program: function() {
920
+ Program: function () {
656
921
  const sourceCode = contextSourceCode(context);
657
922
  if (!hasHeader(sourceCode.text)) {
658
923
  const hasShebang = sourceCode.text.startsWith("#!");
@@ -669,41 +934,10 @@ const headerRule = {
669
934
  }
670
935
  },
671
936
  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
937
  fix: canFix
703
- ? genReplaceFixer(
704
- commentType,
705
- context,
706
- leadingComments,
938
+ ? genPrependFixer(
939
+ headerMatcher.commentType,
940
+ sourceCode,
707
941
  fixLines,
708
942
  eol,
709
943
  numLines)
@@ -711,365 +945,25 @@ const headerRule = {
711
945
  });
712
946
  return;
713
947
  }
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
-
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
- }
899
-
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
- });
929
- return;
930
- }
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;
995
- }
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;
1016
- }
1017
- }
1018
- }
1019
-
1020
- if (!errorMessageId && leadingLines.length > headerLines.length) {
1021
- errorMessageId = "headerTooLong";
1022
- assertDefined(firstLeadingCommentLoc);
1023
- assertNotNull(firstLeadingCommentLoc);
1024
- assertDefined(lastLeadingCommentLoc);
1025
- assertNotNull(lastLeadingCommentLoc);
1026
- errorMessageLoc = {
1027
- start: {
1028
- column: (headerLines.length === 0 ? "/*".length : 0) + 0,
1029
- line: firstLeadingCommentLoc.start.line + headerLines.length
1030
- },
1031
- end: {
1032
- column: lastLeadingCommentLoc.end.column - "*/".length,
1033
- line: lastLeadingCommentLoc.end.line
1034
- }
1035
- };
1036
- }
1037
-
1038
- if (errorMessageId) {
1039
- if (canFix && headerLines.length > 1) {
1040
- 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);
1041
965
  }
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
- });
966
+ context.report(report);
1073
967
  }
1074
968
  }
1075
969
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tony.ganchev/eslint-plugin-header",
3
- "version": "3.2.5",
3
+ "version": "3.2.6",
4
4
  "description": "The native ESLint 9/10 header plugin. A zero-bloat, drop-in replacement for 'eslint-plugin-header' with first-class Flat Config & TypeScript support. Auto-fix Copyright, License, and banner comments in JavaScrip and TypeScript files.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -18,19 +18,20 @@
18
18
  "CONTRIBUTING.md"
19
19
  ],
20
20
  "devDependencies": {
21
+ "@eslint/core": "^1.1.1",
21
22
  "@eslint/js": "^10.0.1",
22
23
  "@eslint/markdown": "^7.5.1",
23
- "@stylistic/eslint-plugin": "^5.9.0",
24
+ "@stylistic/eslint-plugin": "^5.10.0",
24
25
  "@types/estree": "^1.0.8",
25
26
  "@types/json-schema": "^7.0.15",
26
- "@types/node": "^25.3.0",
27
- "c8": "^10.1.3",
28
- "eslint": "^10.0.2",
29
- "eslint-plugin-eslint-plugin": "^7.3.1",
27
+ "@types/node": "^25.3.5",
28
+ "c8": "^11.0.0",
29
+ "eslint": "^10.0.3",
30
+ "eslint-plugin-eslint-plugin": "^7.3.2",
30
31
  "eslint-plugin-jsdoc": "^62.7.1",
31
32
  "eslint-plugin-n": "^17.24.0",
32
- "globals": "^17.3.0",
33
- "markdownlint-cli": "^0.47.0",
33
+ "globals": "^17.4.0",
34
+ "markdownlint-cli": "^0.48.0",
34
35
  "mocha": "12.0.0-beta-9",
35
36
  "testdouble": "^3.20.2",
36
37
  "typescript": "^5.9.3",
@@ -1 +1 @@
1
- {"version":3,"file":"header.d.ts","sourceRoot":"","sources":["../../../lib/rules/header.js"],"names":[],"mappings":";2BAqCa,iBAAiB;0BACjB,gBAAgB;wBAChB,cAAc;0BACd,gBAAgB;;;;;yBAIhB,IAAI,GAAG,MAAM;;;;;;;;;;aAOZ,MAAM,GAAG,MAAM;;;;;;;;;;;;yBAOhB,MAAM,GAAG,MAAM,GAAG,iBAAiB;;;;;0BAGnC,UAAU,GAAG,UAAU,EAAE;;;;;;+BAEzB,IAAI,GAAG,MAAM,GAAG,SAAS;;;;;6BAGzB;IAAE,WAAW,CAAC,EAAE,gBAAgB,CAAA;CAAE;;;;;0BAElC,OAAO,GAAG,MAAM;;;;;;;;;;UAOf,MAAM;;;;;;;;;;;;;;;iBASN,WAAW;;;;;;WACX,UAAU,EAAE;;;;;;;;;;;;;;;;;;YAcZ,eAAe,GAAG,YAAY;;;;;;;;;;;4BAO/B,4BAA4B,GAAG,cAAc;oCAK7C,CAAC,QAAQ,EAAE,MAAM,CAAC;4CAClB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,CAAC;iCAE5C,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,CAAC;yCACvC,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,CAAC;yCAEjE,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,CAAC;iDAEzD,CACV,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,cAAc,CACvB;;;;+BACS,CAAC,aAAa,CAAC,GACvB,qBAAqB,GACrB,6BAA6B,GAC7B,kBAAkB,GAClB,0BAA0B,GAC1B,0BAA0B,GAC1B,kCAAkC;;;;;+BAK1B,iBAAiB,gBAAgB,CAAC;AAqb/C,8BAA8B;AAC9B,0BADW,eAAe,CAufxB;0BAjhC+B,QAAQ;4BAAR,QAAQ"}
1
+ {"version":3,"file":"header.d.ts","sourceRoot":"","sources":["../../../lib/rules/header.js"],"names":[],"mappings":";2BAsCa,iBAAiB;0BACjB,gBAAgB;wBAChB,cAAc;0BACd,gBAAgB;;;;;yBAIhB,IAAI,GAAG,MAAM;;;;;;;;;;aAOZ,MAAM,GAAG,MAAM;;;;;;;;;;;;yBAOhB,MAAM,GAAG,MAAM,GAAG,iBAAiB;;;;;0BAGnC,UAAU,GAAG,UAAU,EAAE;;;;;;+BAEzB,IAAI,GAAG,MAAM,GAAG,SAAS;;;;;6BAGzB;IAAE,WAAW,CAAC,EAAE,gBAAgB,CAAA;CAAE;;;;;0BAElC,OAAO,GAAG,MAAM;;;;;;;;;;UAOf,MAAM;;;;;;;;;;;;;;;iBASN,WAAW;;;;;;WACX,UAAU,EAAE;;;;;;;;;;;;;;;;;;YAcZ,eAAe,GAAG,YAAY;;;;;;;;;;;4BAO/B,4BAA4B,GAAG,cAAc;oCAK7C,CAAC,QAAQ,EAAE,MAAM,CAAC;4CAClB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,CAAC;iCAE5C,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,CAAC;yCACvC,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,CAAC;yCAEjE,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,CAAC;iDAEzD,CACV,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,cAAc,CACvB;;;;+BACS,CAAC,aAAa,CAAC,GACvB,qBAAqB,GACrB,6BAA6B,GAC7B,kBAAkB,GAClB,0BAA0B,GAC1B,0BAA0B,GAC1B,kCAAkC;;;;;+BAK1B,iBAAiB,gBAAgB,CAAC;AA0sB/C,8BAA8B;AAC9B,0BADW,eAAe,CAuHxB;0BAv6B4D,QAAQ;4BAAR,QAAQ"}