@tony.ganchev/eslint-plugin-header 3.2.6 → 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.
package/README.md CHANGED
@@ -20,7 +20,9 @@ and banner comments in JavaScript and TypeScript files.
20
20
  2. [Providing To-year in Auto-fix](#providing-to-year-in-auto-fix)
21
21
  3. [Trailing Empty Lines Configuration](#trailing-empty-lines-configuration)
22
22
  4. [Line Endings](#line-endings)
23
- 3. [Examples](#examples)
23
+ 3. [Support for Leading Comments](#support-for-leading-comments)
24
+ 1. [Notes on Behavior](#notes-on-behavior)
25
+ 4. [Examples](#examples)
24
26
  4. [Comparison to Alternatives](#comparison-to-alternatives)
25
27
  1. [Compared to eslint-plugin-headers](#compared-to-eslint-plugin-headers)
26
28
  1. [Health Scans](#health-scans)
@@ -659,6 +661,293 @@ export default defineConfig([
659
661
  Possible values are `"unix"` for `\n` and `"windows"` for `\r\n` line endings.
660
662
  The default value is `"os"` which means assume the system-specific line endings.
661
663
 
664
+ ### Support for Leading Comments
665
+
666
+ _NOTE: This feature is still experimental and as such may break between minor
667
+ versions and revisions._
668
+
669
+ _NOTE: This feature will **only** be available with the modern object-based
670
+ configuration._
671
+
672
+ Some frameworks such as [Jest](https://jestjs.io/) change behavior based on
673
+ pragma comments such as:
674
+
675
+ ```js
676
+ /** @jest-environement node */
677
+ ```
678
+
679
+ The problem with these is that they are not part of the header comment and
680
+ should be allowed to appear before the header comment. The `leadingComments`
681
+ option allows you to specify a set of comments that are allowed to appear before
682
+ the header comment. It is configured as an array of comments-matching rules
683
+ similar to the `header` section. For example to match the following header with
684
+ a leading pragma:
685
+
686
+ ```js
687
+ /** @jest-environement node */
688
+ /* Copyright 2015, My Company */
689
+ ```
690
+
691
+ ... we can use the following configuration:
692
+
693
+ ```ts
694
+ import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
695
+ import { defineConfig } from "eslint/config";
696
+
697
+ export default defineConfig([
698
+ {
699
+ files: ["**/*.js"],
700
+ plugins: {
701
+ "@tony.ganchev": header
702
+ },
703
+ rules: {
704
+ "@tony.ganchev/header": [
705
+ "error",
706
+ {
707
+ header: {
708
+ commentType: "block",
709
+ lines: [" Copyright 2015, My Company "]
710
+ },
711
+ leadingComments: {
712
+ comments: [
713
+ {
714
+ commentType: "block",
715
+ lines: ["* @jest-environement node "]
716
+ }
717
+ ]
718
+ }
719
+ } as HeaderOptions
720
+ ]
721
+ }
722
+ }
723
+ ]);
724
+ ```
725
+
726
+ Assuming you need to tolerate more pragmas, you can have a longer list of
727
+ comments e.g.
728
+
729
+ ```ts
730
+ import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
731
+ import { defineConfig } from "eslint/config";
732
+
733
+ export default defineConfig([
734
+ {
735
+ files: ["**/*.js"],
736
+ plugins: {
737
+ "@tony.ganchev": header
738
+ },
739
+ rules: {
740
+ "@tony.ganchev/header": [
741
+ "error",
742
+ {
743
+ header: {
744
+ commentType: "block",
745
+ lines: [" Copyright 2015, My Company "]
746
+ },
747
+ leadingComments: {
748
+ comments: [
749
+ {
750
+ commentType: "block",
751
+ lines: ["* @jest-environement node "]
752
+ },
753
+ {
754
+ commentType: "line",
755
+ lines: [" @ts-ignore"]
756
+ }
757
+ ]
758
+ }
759
+ } as HeaderOptions
760
+ ]
761
+ }
762
+ }
763
+ ]);
764
+ ```
765
+
766
+ You can also use file-based configuration for any of these allowed comments.
767
+
768
+ #### Notes on Behavior
769
+
770
+ There are a number of things to consider when validating headers when allowing
771
+ some leading comments. It is important to understand the algorithm behind.
772
+ During validation, the rule breaks up all comments before the first actual code
773
+ token based either on the beginning and end of a block comments or based on the
774
+ separation of line comments by more than one line. These discrete comment blocks
775
+ are then validated against both the header-matching rule and all the leading
776
+ comment-matching rules.
777
+
778
+ For each comment, header is tested first and if it matches, validation completes
779
+ successfully. If not, the algorithm verifies that the comment satisfies at least
780
+ one comment matcher and if so, validation moves to the next comment.
781
+
782
+ If the comment matches neither the header, nor any of the leading comment
783
+ matchers, validation fails. To provide good troubleshooting information, errors
784
+ are reported for the header matcher, followed by all leading comment matchers.
785
+ While the information may seem overwhelming, this helps developers understand
786
+ all possible failures and let them pick the essential one.
787
+
788
+ Let's have the following configuration example:
789
+
790
+ ```ts
791
+ import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
792
+ import { defineConfig } from "eslint/config";
793
+
794
+ export default defineConfig([
795
+ {
796
+ files: ["**/*.js"],
797
+ plugins: {
798
+ "@tony.ganchev": header
799
+ },
800
+ rules: {
801
+ "@tony.ganchev/header": [
802
+ "error",
803
+ {
804
+ header: {
805
+ commentType: "block",
806
+ lines: [" Copyright 2015, My Company "]
807
+ },
808
+ leadingComments: {
809
+ comments: [
810
+ {
811
+ commentType: "block",
812
+ lines: ["* @jest-environement node "]
813
+ },
814
+ {
815
+ commentType: "line",
816
+ lines: [" @ts-ignore"]
817
+ }
818
+ ]
819
+ }
820
+ } as HeaderOptions
821
+ ]
822
+ }
823
+ }
824
+ ]);
825
+ ```
826
+
827
+ Let's lint the following piece of code:
828
+
829
+ ```js
830
+ /** @jest-environement node */
831
+ /* Copyright 2010, My Company */
832
+
833
+ console.log(1);
834
+ ```
835
+
836
+ The following errors would be shown:
837
+
838
+ ```bash
839
+ 2:1 error leading comment validation failed: should be a line comment
840
+ @tony.ganchev/header
841
+ 2:3 error header line does not match expected after this position;
842
+ expected: 'Copyright 2015, My Company'
843
+ @tony.ganchev/header
844
+ 2:3 error leading comment validation failed: line does not match expected
845
+ after this position; expected: '* @jest-environement node '
846
+ @tony.ganchev/header
847
+ ```
848
+
849
+ Notice how all errors are reported on the second line. That is because the first
850
+ line passes validation against the first leading comment matcher, while the
851
+ second fails validation against all matchers.
852
+
853
+ Requiring an empty line between line leading comments is important as it keeps
854
+ the rule simple and fast but needs to be kept into account. Let's take the
855
+ following configuration for example:
856
+
857
+ ```ts
858
+ import header, { HeaderOptions } from "@tony.ganchev/eslint-plugin-header";
859
+ import { defineConfig } from "eslint/config";
860
+
861
+ export default defineConfig([
862
+ {
863
+ files: ["**/*.js"],
864
+ plugins: {
865
+ "@tony.ganchev": header
866
+ },
867
+ rules: {
868
+ "@tony.ganchev/header": [
869
+ "error",
870
+ {
871
+ header: {
872
+ commentType: "block",
873
+ lines: [" Copyright 2015, My Company "]
874
+ },
875
+ leadingComments: {
876
+ comments: [
877
+ {
878
+ commentType: "line",
879
+ lines: [" foo"]
880
+ },
881
+ {
882
+ commentType: "line",
883
+ lines: [" bar"]
884
+ }
885
+ ]
886
+ }
887
+ } as HeaderOptions
888
+ ]
889
+ }
890
+ }
891
+ ]);
892
+ ```
893
+
894
+ This configuration would successfully lint any of the following snippets:
895
+
896
+ ```js
897
+ // foo
898
+
899
+ // bar
900
+ /* Copyright 2015, My Company */
901
+ console.log();
902
+ ```
903
+
904
+ ```js
905
+ // bar
906
+
907
+ // foo
908
+
909
+ /* Copyright 2015, My Company */
910
+ console.log();
911
+ ```
912
+
913
+ ```js
914
+ // bar
915
+
916
+ // bar
917
+ /* Copyright 2015, My Company */
918
+ console.log();
919
+ ```
920
+
921
+ It will not pass the following snippets though:
922
+
923
+ ```js
924
+ // foo
925
+ // bar
926
+
927
+ /* Copyright 2015, My Company */
928
+ console.log();
929
+ ```
930
+
931
+ ```js
932
+ // bar
933
+ // foo
934
+ /* Copyright 2015, My Company */
935
+ console.log();
936
+ ```
937
+
938
+ ```js
939
+ // bar
940
+ // bar
941
+
942
+ /* Copyright 2015, My Company */
943
+ console.log();
944
+ ```
945
+
946
+ Finally, it is worth noting that the current version accepts an arbitrary number
947
+ of empty lines in between comments. The only expectation still in place is that
948
+ there is no empty line after a shebang comment. Any of these details may change
949
+ through configuration in the future.
950
+
662
951
  ### Examples
663
952
 
664
953
  The following examples are all valid.
@@ -937,4 +1226,4 @@ Backward-compatibility does not cover the following functional aspects:
937
1226
 
938
1227
  ## License
939
1228
 
940
- MIT
1229
+ MIT, see [license file](./LICENSE.md) for more details.
@@ -89,6 +89,14 @@ const { lineEndingOptions, commentTypeOptions, schema } = require("./header.sche
89
89
  * lines together.
90
90
  */
91
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
+
92
100
  /**
93
101
  * @typedef {object} TrailingEmptyLines Rule configuration on the handling of
94
102
  * empty lines after the header comment.
@@ -100,6 +108,9 @@ const { lineEndingOptions, commentTypeOptions, schema } = require("./header.sche
100
108
  * @typedef {object} HeaderOptionsWithoutSettings
101
109
  * @property {FileBasedConfig | InlineConfig} header The text matching rules
102
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.
103
114
  * @property {TrailingEmptyLines} [trailingEmptyLines] Rules about empty lines
104
115
  * after the header comment.
105
116
  */
@@ -168,42 +179,66 @@ function match(actual, expected) {
168
179
  }
169
180
 
170
181
  /**
171
- * Remove Unix she-bangs from the list of comments.
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.
176
- */
177
- function excludeShebangs(comments) {
178
- /** @type {Comment[]} */
179
- return comments.filter(function (comment) {
180
- return comment.type !== "Shebang";
181
- });
182
- }
183
-
184
- /**
185
- * Returns either the first block comment or the first set of line comments that
186
- * are ONLY separated by a single newline. Note that this does not actually
187
- * check if they are at the start of the file since that is already checked by
188
- * `hasHeader()`.
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.
189
185
  * @param {SourceCode} sourceCode AST.
190
- * @returns {Comment[]} Lines That constitute the leading comment.
186
+ * @returns {Comment[][]} Array of groups of leading comments.
191
187
  */
192
188
  function getLeadingComments(sourceCode) {
193
- const all = excludeShebangs(sourceCode.getAllComments());
194
- if (all[0].type.toLowerCase() === commentTypeOptions.block) {
195
- return [all[0]];
189
+ const all = sourceCode.getAllComments();
190
+ if (all.length === 0) {
191
+ return [];
196
192
  }
197
- let i = 1;
198
- for (; i < all.length; ++i) {
199
- const previousRange = /** @type {[number, number]} */ (all[i - 1].range);
200
- const currentRange = /** @type {[number, number]} */ (all[i].range);
201
- const txt = sourceCode.text.slice(previousRange[1], currentRange[0]);
202
- if (!txt.match(/^(\r\n|\r|\n)$/)) {
203
- break;
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 = [];
203
+ /** @type {Comment[]} */
204
+ let currentGroup = [];
205
+ for (let i = 0; i < commentsBeforeCode.length; ++i) {
206
+ const comment = commentsBeforeCode[i];
207
+
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]);
223
+
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
+ }
204
234
  }
205
235
  }
206
- return all.slice(0, i);
236
+
237
+ if (currentGroup.length > 0) {
238
+ groups.push(currentGroup);
239
+ }
240
+
241
+ return groups;
207
242
  }
208
243
 
209
244
  /**
@@ -351,17 +386,6 @@ function getEol(style) {
351
386
  }
352
387
  }
353
388
 
354
- /**
355
- * Tests if the first line in the source code (after a Unix she-bang) is a
356
- * comment. Does not tolerate empty lines before the first match.
357
- * @param {string} src Source code to test.
358
- * @returns {boolean} `true` if there is a comment or `false` otherwise.
359
- */
360
- function hasHeader(src) {
361
- const srcWithoutShebang = src.replace(/^#![^\n]*\r?\n/, "");
362
- return srcWithoutShebang.startsWith("/*") || srcWithoutShebang.startsWith("//");
363
- }
364
-
365
389
  /**
366
390
  * Asserts on an expression and adds template texts to the failure message.
367
391
  * Helper to write cleaner code.
@@ -433,14 +457,14 @@ function transformLegacyOptions(originalOptions) {
433
457
  transformedOptions.trailingEmptyLines = { minimum: originalOptions[2] };
434
458
  if (originalOptions.length === 4) {
435
459
  schemaAssert(typeof originalOptions[3] === "object",
436
- "Fourth header option after severity should be either number of required trailing empty lines or " +
437
- "a settings object");
460
+ "Fourth header option after severity should be either number of required trailing empty lines or "
461
+ + "a settings object");
438
462
  Object.assign(transformedOptions, originalOptions[3]);
439
463
  }
440
464
  } else {
441
465
  schemaAssert(typeof originalOptions[2] === "object",
442
- "Third header option after severity should be either number of required trailing empty lines or a " +
443
- "settings object");
466
+ "Third header option after severity should be either number of required trailing empty lines or a "
467
+ + "settings object");
444
468
  Object.assign(transformedOptions, originalOptions[2]);
445
469
  }
446
470
  }
@@ -454,7 +478,7 @@ function transformLegacyOptions(originalOptions) {
454
478
  * else `false`.
455
479
  */
456
480
  function isFileBasedHeaderConfig(config) {
457
- return Object.prototype.hasOwnProperty.call(config, "file");
481
+ return "file" in config;
458
482
  }
459
483
 
460
484
  /**
@@ -506,6 +530,13 @@ function normalizeOptions(originalOptions) {
506
530
  options.lineEndings = "os";
507
531
  }
508
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
+ }
509
540
  if (!options.trailingEmptyLines) {
510
541
  options.trailingEmptyLines = {};
511
542
  }
@@ -561,13 +592,18 @@ class CommentMatcher {
561
592
  this.numLines = numLines;
562
593
  }
563
594
 
595
+ /**
596
+ * @typedef {ViolationReport<JSSyntaxElement, string>} ViolationReportBad
597
+ * @typedef {ViolationReportBad & { messageId: string }} ViolationReportEx
598
+ */
599
+
564
600
  /**
565
601
  * Performs a validation of a comment against a header matching
566
602
  * configuration.
567
603
  * @param {Comment[]} leadingComments The block comment or sequence of line
568
604
  * comments to test.
569
605
  * @param {SourceCode} sourceCode The source code AST.
570
- * @returns {ViolationReport<JSSyntaxElement, string> | null} If set a
606
+ * @returns {ViolationReportEx | null} If set a
571
607
  * violation report to pass back to ESLint or interpret as necessary.
572
608
  */
573
609
  validate(leadingComments, sourceCode) {
@@ -602,6 +638,9 @@ class CommentMatcher {
602
638
  end: lastLeadingCommentLoc.end
603
639
  },
604
640
  messageId: "incorrectHeader",
641
+ data: {
642
+ pattern: this.headerLines[0].toString()
643
+ }
605
644
  };
606
645
  }
607
646
  } else {
@@ -869,14 +908,33 @@ const headerRule = {
869
908
  }
870
909
  ],
871
910
  messages: {
872
- 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}}'",
873
914
  headerLineTooLong: "header line longer than expected",
874
- headerLineTooShort: "header line shorter than expected; missing: {{remainder}}",
875
- headerTooShort: "header too short: missing lines: {{remainder}}",
915
+ headerLineTooShort: "header line shorter than expected; missing: '{{remainder}}'",
916
+ headerTooShort: "header too short; missing lines: '{{remainder}}'",
876
917
  headerTooLong: "header too long",
877
918
  incorrectCommentType: "header should be a {{commentType}} comment",
878
- incorrectHeader: "incorrect header",
879
- 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.
880
938
  missingHeader: "missing header",
881
939
  noNewlineAfterHeader: "not enough newlines after header: expected: {{expected}}, actual: {{actual}}"
882
940
  }
@@ -891,25 +949,20 @@ const headerRule = {
891
949
 
892
950
  const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */(context.options));
893
951
  const options = normalizeOptions(newStyleOptions);
894
-
895
- const eol = getEol(
896
- /** @type {LineEndingOption} */(options.lineEndings)
897
- );
898
-
952
+ const eol = getEol(/** @type {LineEndingOption} */(options.lineEndings));
899
953
  const header = /** @type {InlineConfig} */ (options.header);
900
-
901
954
  const canFix = !header.lines.some((line) => isPattern(line) && !("template" in line));
902
-
903
955
  const fixLines = header.lines.map((line) => {
904
956
  if (isPattern(line)) {
905
957
  return ("template" in line) ? /** @type {string} */(line.template) : "";
906
958
  }
907
959
  return /** @type {string} */(line);
908
960
  });
909
-
910
961
  const numLines = /** @type {number} */ (options.trailingEmptyLines?.minimum);
911
-
912
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));
913
966
 
914
967
  return {
915
968
  /**
@@ -919,18 +972,27 @@ const headerRule = {
919
972
  */
920
973
  Program: function () {
921
974
  const sourceCode = contextSourceCode(context);
922
- if (!hasHeader(sourceCode.text)) {
923
- const hasShebang = sourceCode.text.startsWith("#!");
924
- 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
+
925
987
  context.report({
926
988
  loc: {
927
989
  start: {
928
- column: 1,
929
- line
990
+ column: 0,
991
+ line: startingHeaderLine
930
992
  },
931
993
  end: {
932
- column: 1,
933
- line
994
+ column: 0,
995
+ line: startingHeaderLine
934
996
  }
935
997
  },
936
998
  messageId: "missingHeader",
@@ -945,26 +1007,69 @@ const headerRule = {
945
1007
  });
946
1008
  return;
947
1009
  }
948
- const leadingComments = getLeadingComments(sourceCode);
949
1010
 
950
- const report = headerMatcher.validate(leadingComments, sourceCode);
1011
+ for (const leadingComment of leadingComments) {
1012
+
1013
+ const headerReport = headerMatcher.validate(leadingComment, sourceCode);
1014
+ if (headerReport === null) {
1015
+ return;
1016
+ }
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);
1034
+ }
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
+ }
1043
+ }
1044
+ return;
1045
+ }
1046
+ }
1047
+
1048
+ const lastComment = leadingComments[leadingComments.length - 1];
1049
+ const lastCommentLine = lastComment[lastComment.length - 1];
1050
+ const lineIndex = /** @type {number} */ (lastCommentLine.loc?.end.line) + 1;
951
1051
 
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(
1052
+ context.report({
1053
+ loc: {
1054
+ start: {
1055
+ column: 0,
1056
+ line: lineIndex
1057
+ },
1058
+ end: {
1059
+ column: 0,
1060
+ line: lineIndex
1061
+ }
1062
+ },
1063
+ messageId: "missingHeader",
1064
+ fix: canFix
1065
+ ? genPrependFixer(
959
1066
  headerMatcher.commentType,
960
1067
  sourceCode,
961
- leadingComments,
962
1068
  fixLines,
963
1069
  eol,
964
- numLines);
965
- }
966
- context.report(report);
967
- }
1070
+ numLines)
1071
+ : null
1072
+ });
968
1073
  }
969
1074
  };
970
1075
  }
@@ -142,6 +142,28 @@ const schema = Object.freeze({
142
142
  required: ["commentType", "lines"],
143
143
  additionalProperties: false
144
144
  },
145
+ header: {
146
+ anyOf: [
147
+ { $ref: "#/definitions/fileBasedHeader" },
148
+ { $ref: "#/definitions/inlineHeader" }
149
+ ],
150
+ description: "Header comment matching rules."
151
+ },
152
+ leadingComments: {
153
+ type: "object",
154
+ properties: {
155
+ comments: {
156
+ type: "array",
157
+ items: { $ref: "#/definitions/header" },
158
+ description: "The set of comment matching rules. The rule can match one or more comments against " +
159
+ "these rules."
160
+ }
161
+ },
162
+ required: ["comments"],
163
+ additionalProperties: false,
164
+ description: "Set of comments that can appear before the header comment. Useful for pragmas used by some " +
165
+ "tools that expect these to be the first comment in the file."
166
+ },
145
167
  trailingEmptyLines: {
146
168
  type: "object",
147
169
  properties: {
@@ -156,12 +178,8 @@ const schema = Object.freeze({
156
178
  newOptions: {
157
179
  type: "object",
158
180
  properties: {
159
- header: {
160
- anyOf: [
161
- { $ref: "#/definitions/fileBasedHeader" },
162
- { $ref: "#/definitions/inlineHeader" }
163
- ]
164
- },
181
+ header: { $ref: "#/definitions/header" },
182
+ leadingComments: { $ref: "#/definitions/leadingComments" },
165
183
  lineEndings: { $ref: "#/definitions/lineEndings" },
166
184
  trailingEmptyLines: { $ref: "#/definitions/trailingEmptyLines" }
167
185
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tony.ganchev/eslint-plugin-header",
3
- "version": "3.2.6",
3
+ "version": "3.3.0",
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",
@@ -85,6 +85,18 @@ export type InlineConfig = {
85
85
  */
86
86
  lines: HeaderLine[];
87
87
  };
88
+ /**
89
+ * A set of comments that can appear before
90
+ * the header.
91
+ */
92
+ export type LeadingComments = {
93
+ /**
94
+ * The set of comments
95
+ * that are allowed. If none of the matching rules matches the first comment the
96
+ * rule assumes the first comment *is* the header.
97
+ */
98
+ comments: (FileBasedConfig | InlineConfig)[];
99
+ };
88
100
  /**
89
101
  * Rule configuration on the handling of
90
102
  * empty lines after the header comment.
@@ -102,6 +114,12 @@ export type HeaderOptionsWithoutSettings = {
102
114
  * for the header.
103
115
  */
104
116
  header: FileBasedConfig | InlineConfig;
117
+ /**
118
+ * The set of allowed comments to
119
+ * precede the header. Useful to allow position-sensitive pragma comments for
120
+ * certain tools.
121
+ */
122
+ leadingComments?: LeadingComments | undefined;
105
123
  /**
106
124
  * Rules about empty lines
107
125
  * after the header comment.
@@ -1 +1 @@
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"}
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;;;;;;;;;;;;cAQZ,CAAC,eAAe,GAAG,YAAY,CAAC,EAAE;;;;;;;;;;;;;;;;;;YAclC,eAAe,GAAG,YAAY;;;;;;;;;;;;;;;;;4BAU/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;AAsuB/C,8BAA8B;AAC9B,0BADW,eAAe,CAyLxB;0BAhhC4D,QAAQ;4BAAR,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"header.schema.d.ts","sourceRoot":"","sources":["../../../lib/rules/header.schema.js"],"names":[],"mappings":"gCA4BU,MAAM;AADhB;;GAEG;AACH;;;;GAIG;iCAGO,MAAM;AADhB;;GAEG;AACH;;;GAGG;AAEH,gDAAgD;AAChD,qBADW,OAAO,aAAa,EAAE,WAAW,CAwKzC"}
1
+ {"version":3,"file":"header.schema.d.ts","sourceRoot":"","sources":["../../../lib/rules/header.schema.js"],"names":[],"mappings":"gCA4BU,MAAM;AADhB;;GAEG;AACH;;;;GAIG;iCAGO,MAAM;AADhB;;GAEG;AACH;;;GAGG;AAEH,gDAAgD;AAChD,qBADW,OAAO,aAAa,EAAE,WAAW,CA0LzC"}