@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 +291 -2
- package/lib/rules/header.js +187 -82
- package/lib/rules/header.schema.js +24 -6
- package/package.json +1 -1
- package/types/lib/rules/header.d.ts +18 -0
- package/types/lib/rules/header.d.ts.map +1 -1
- package/types/lib/rules/header.schema.d.ts.map +1 -1
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. [
|
|
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.
|
package/lib/rules/header.js
CHANGED
|
@@ -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
|
-
*
|
|
172
|
-
*
|
|
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[]}
|
|
186
|
+
* @returns {Comment[][]} Array of groups of leading comments.
|
|
191
187
|
*/
|
|
192
188
|
function getLeadingComments(sourceCode) {
|
|
193
|
-
const all =
|
|
194
|
-
if (all
|
|
195
|
-
return [
|
|
189
|
+
const all = sourceCode.getAllComments();
|
|
190
|
+
if (all.length === 0) {
|
|
191
|
+
return [];
|
|
196
192
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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:
|
|
929
|
-
line
|
|
990
|
+
column: 0,
|
|
991
|
+
line: startingHeaderLine
|
|
930
992
|
},
|
|
931
993
|
end: {
|
|
932
|
-
column:
|
|
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
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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;;;;;;;;;;;;;;;;;;
|
|
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,
|
|
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"}
|