@tony.ganchev/eslint-plugin-header 3.1.9 → 3.1.11
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/LICENSE.md +4 -2
- package/README.md +0 -3
- package/index.js +3 -3
- package/lib/comment-parser.js +16 -14
- package/lib/rules/header.docs.js +40 -0
- package/lib/rules/header.js +486 -275
- package/lib/rules/header.schema.js +135 -0
- package/package.json +13 -7
package/LICENSE.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-present Stuart Knightley, Tony Ganchev, and contributors
|
|
2
4
|
|
|
3
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
6
|
this software and associated documentation files (the “Software”), to deal in
|
|
@@ -15,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
|
15
17
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
16
18
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
19
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
18
|
-
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -112,7 +112,6 @@ Suppose we want our header to look like this:
|
|
|
112
112
|
* Copyright (c) 2015
|
|
113
113
|
* My Company
|
|
114
114
|
*/
|
|
115
|
-
...
|
|
116
115
|
```
|
|
117
116
|
|
|
118
117
|
All of the following configurations will match the header:
|
|
@@ -211,7 +210,6 @@ perfectly valid, such as:
|
|
|
211
210
|
* Copyright 2020
|
|
212
211
|
* My company
|
|
213
212
|
*/
|
|
214
|
-
...
|
|
215
213
|
```
|
|
216
214
|
|
|
217
215
|
Moreover, suppose your legal department expects that the year of first and last
|
|
@@ -223,7 +221,6 @@ support:
|
|
|
223
221
|
* Copyright 2017-2022
|
|
224
222
|
* My company
|
|
225
223
|
*/
|
|
226
|
-
...
|
|
227
224
|
```
|
|
228
225
|
|
|
229
226
|
We can use a regular expression to support all of these cases for your header:
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* MIT License
|
|
3
3
|
*
|
|
4
|
-
* Copyright (c) 2015-present Stuart Knightley and contributors
|
|
4
|
+
* Copyright (c) 2015-present Stuart Knightley, Tony Ganchev, and contributors
|
|
5
5
|
*
|
|
6
6
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
* of this software and associated documentation files (the “Software”), to deal
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* copies of the Software, and to permit persons to whom the Software is
|
|
11
11
|
* furnished to do so, subject to the following conditions:
|
|
12
12
|
*
|
|
13
|
-
* The above copyright notice and this permission notice shall be included in
|
|
14
|
-
* copies or substantial portions of the Software.
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in
|
|
14
|
+
* all copies or substantial portions of the Software.
|
|
15
15
|
*
|
|
16
16
|
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
17
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
package/lib/comment-parser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* MIT License
|
|
3
3
|
*
|
|
4
|
-
* Copyright (c) 2015-present Stuart Knightley and contributors
|
|
4
|
+
* Copyright (c) 2015-present Stuart Knightley, Tony Ganchev, and contributors
|
|
5
5
|
*
|
|
6
6
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
* of this software and associated documentation files (the “Software”), to deal
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* copies of the Software, and to permit persons to whom the Software is
|
|
11
11
|
* furnished to do so, subject to the following conditions:
|
|
12
12
|
*
|
|
13
|
-
* The above copyright notice and this permission notice shall be included in
|
|
14
|
-
* copies or substantial portions of the Software.
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in
|
|
14
|
+
* all copies or substantial portions of the Software.
|
|
15
15
|
*
|
|
16
16
|
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
17
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
@@ -24,30 +24,32 @@
|
|
|
24
24
|
|
|
25
25
|
"use strict";
|
|
26
26
|
|
|
27
|
+
const assert = require("assert");
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
|
-
* Parses a line or block comment and returns the type of comment and an array
|
|
30
|
+
* Parses a line or block comment and returns the type of comment and an array
|
|
31
|
+
* of content lines.
|
|
29
32
|
*
|
|
30
33
|
* This is a really simple and dumb parser, that looks just for a
|
|
31
34
|
* single kind of comment. It won't detect multiple block comments.
|
|
32
35
|
* @param {string} commentText comment text.
|
|
33
|
-
* @returns {['block' | 'line', string[]]} comment type and comment
|
|
36
|
+
* @returns {['block' | 'line', string | string[]]} comment type and comment
|
|
37
|
+
* content broken into lines.
|
|
34
38
|
*/
|
|
35
39
|
module.exports = function commentParser(commentText) {
|
|
40
|
+
assert.strictEqual(typeof commentText, "string");
|
|
36
41
|
const text = commentText.trim();
|
|
37
42
|
|
|
38
|
-
if (text.
|
|
43
|
+
if (text.startsWith("//")) {
|
|
39
44
|
return [
|
|
40
45
|
"line",
|
|
41
|
-
text.split(/\r?\n/).map(
|
|
42
|
-
return line.substr(2);
|
|
43
|
-
})
|
|
46
|
+
text.split(/\r?\n/).map((line) => line.substring(2))
|
|
44
47
|
];
|
|
45
|
-
} else if (
|
|
46
|
-
text.substr(0, 2) === "/*" &&
|
|
47
|
-
text.substr(-2) === "*/"
|
|
48
|
-
) {
|
|
48
|
+
} else if (text.startsWith("/*") && text.endsWith("*/")) {
|
|
49
49
|
return ["block", text.substring(2, text.length - 2)];
|
|
50
50
|
} else {
|
|
51
|
-
throw new Error(
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Could not parse comment file: the file must contain either just line comments (//) or a single block " +
|
|
53
|
+
"comment (/* ... */)");
|
|
52
54
|
}
|
|
53
55
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MIT License
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025-present Tony Ganchev and contributors
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the “Software”), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in
|
|
14
|
+
* all copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
"use strict";
|
|
27
|
+
|
|
28
|
+
const assert = require("node:assert");
|
|
29
|
+
const fs = require("node:fs");
|
|
30
|
+
const path = require("node:path");
|
|
31
|
+
|
|
32
|
+
const packageJsonContent = fs.readFileSync(path.resolve(__dirname, "../../package.json"));
|
|
33
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
34
|
+
assert.equal(Object.prototype.hasOwnProperty.call(packageJson, "version"), true,
|
|
35
|
+
"The plugin's package.json should be available two directories above the rule.");
|
|
36
|
+
const pluginVersion = packageJsonContent.version;
|
|
37
|
+
|
|
38
|
+
exports.description = "";
|
|
39
|
+
exports.recommended = true;
|
|
40
|
+
exports.url = "https://www.npmjs.com/package/@tony.ganchev/eslint-plugin-header/v/" + pluginVersion;
|
package/lib/rules/header.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* MIT License
|
|
3
3
|
*
|
|
4
|
-
* Copyright (c) 2015-present Stuart Knightley and contributors
|
|
4
|
+
* Copyright (c) 2015-present Stuart Knightley, Tony Ganchev, and contributors
|
|
5
5
|
*
|
|
6
6
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
* of this software and associated documentation files (the “Software”), to deal
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* copies of the Software, and to permit persons to whom the Software is
|
|
11
11
|
* furnished to do so, subject to the following conditions:
|
|
12
12
|
*
|
|
13
|
-
* The above copyright notice and this permission notice shall be included in
|
|
14
|
-
* copies or substantial portions of the Software.
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in
|
|
14
|
+
* all copies or substantial portions of the Software.
|
|
15
15
|
*
|
|
16
16
|
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
17
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
@@ -24,10 +24,15 @@
|
|
|
24
24
|
|
|
25
25
|
"use strict";
|
|
26
26
|
|
|
27
|
+
const assert = require("assert");
|
|
28
|
+
const fs = require("fs");
|
|
29
|
+
const os = require("os");
|
|
30
|
+
const commentParser = require("../comment-parser");
|
|
31
|
+
const { description, recommended, url } = require("./header.docs");
|
|
32
|
+
const { commentTypeOptions, lineEndingOptions, schema } = require("./header.schema");
|
|
33
|
+
|
|
27
34
|
/**
|
|
28
35
|
* Import type definitions.
|
|
29
|
-
* @typedef {import('estree').Comment} Comment
|
|
30
|
-
* @typedef {import('estree').Program} Program
|
|
31
36
|
* @typedef {import('eslint').Rule.Fix} Fix
|
|
32
37
|
* @typedef {import('eslint').Rule.NodeListener} NodeListener
|
|
33
38
|
* @typedef {import('eslint').Rule.ReportFixer} ReportFixer
|
|
@@ -35,50 +40,44 @@
|
|
|
35
40
|
* @typedef {import('eslint').Rule.RuleTextEdit} RuleTextEdit
|
|
36
41
|
* @typedef {import('eslint').Rule.RuleTextEditor} RuleTextEditor
|
|
37
42
|
* @typedef {import('eslint').Rule.RuleContext} RuleContext
|
|
43
|
+
* @typedef {import('estree').Comment} Comment
|
|
44
|
+
* @typedef {import('estree').Program} Program
|
|
45
|
+
* @typedef {import("estree").SourceLocation} SourceLocation
|
|
38
46
|
*/
|
|
39
47
|
|
|
40
|
-
/**
|
|
41
|
-
* @enum {string}
|
|
42
|
-
*/
|
|
43
|
-
const lineEndingOptions = Object.freeze({
|
|
44
|
-
unix: "unix", // \n
|
|
45
|
-
windows: "windows", // \n
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @enum {string}
|
|
50
|
-
*/
|
|
51
|
-
const commentTypeOptions = Object.freeze({
|
|
52
|
-
block: "block",
|
|
53
|
-
line: "line"
|
|
54
|
-
});
|
|
55
|
-
|
|
56
48
|
/**
|
|
57
49
|
* Local type defintions.
|
|
58
50
|
* @typedef {string | { pattern: string, template?: string }} HeaderLine
|
|
59
51
|
* @typedef {(HeaderLine | HeaderLine[])} HeaderLines
|
|
60
52
|
* @typedef {{ lineEndings: ('unix' | 'windows') }} HeaderSettings
|
|
61
|
-
* @typedef {
|
|
53
|
+
* @typedef {
|
|
54
|
+
* [string]
|
|
55
|
+
* | [string, HeaderSettings]
|
|
56
|
+
* | [('block' | 'line') | HeaderLines ]
|
|
57
|
+
* | [('block' | 'line') | HeaderLines | HeaderSettings]
|
|
58
|
+
* | [('block' | 'line') | HeaderLines | number ]
|
|
59
|
+
* | [('block' | 'line') | HeaderLines | number | HeaderSettings]
|
|
60
|
+
* } HeaderOptions
|
|
62
61
|
*/
|
|
63
62
|
|
|
64
|
-
var fs = require("fs");
|
|
65
|
-
var commentParser = require("../comment-parser");
|
|
66
|
-
var os = require("os");
|
|
67
|
-
|
|
68
63
|
/**
|
|
69
|
-
* Tests if the passed line configuration string or object is a pattern
|
|
64
|
+
* Tests if the passed line configuration string or object is a pattern
|
|
65
|
+
* definition.
|
|
70
66
|
* @param {HeaderLine} object line configuration object or string
|
|
71
|
-
* @returns {boolean} `true` if the line configuration is a pattern-
|
|
67
|
+
* @returns {boolean} `true` if the line configuration is a pattern-defining
|
|
68
|
+
* object or `false` otherwise.
|
|
72
69
|
*/
|
|
73
70
|
function isPattern(object) {
|
|
74
71
|
return typeof object === "object" && Object.prototype.hasOwnProperty.call(object, "pattern");
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
/**
|
|
78
|
-
* Utility over a line config argument to match an expected string either
|
|
75
|
+
* Utility over a line config argument to match an expected string either
|
|
76
|
+
* against a regex or for full match against a string.
|
|
79
77
|
* @param {HeaderLine} actual the string to test.
|
|
80
78
|
* @param {string} expected The string or regex to test again.
|
|
81
|
-
* @returns {boolean} `true` if the passed string matches the expected line
|
|
79
|
+
* @returns {boolean} `true` if the passed string matches the expected line
|
|
80
|
+
* config or `false` otherwise.
|
|
82
81
|
*/
|
|
83
82
|
function match(actual, expected) {
|
|
84
83
|
if (expected.test) {
|
|
@@ -91,7 +90,9 @@ function match(actual, expected) {
|
|
|
91
90
|
/**
|
|
92
91
|
* Remove Unix she-bangs from the list of comments.
|
|
93
92
|
* @param {Comment[]} comments the list of comment lines.
|
|
94
|
-
* @returns {Comment[]} the list of comments with containing all incomming
|
|
93
|
+
* @returns {Comment[]} the list of comments with containing all incomming
|
|
94
|
+
* comments from `comments` with the shebang comments
|
|
95
|
+
* omitted.
|
|
95
96
|
*/
|
|
96
97
|
function excludeShebangs(comments) {
|
|
97
98
|
return comments.filter(function(comment) {
|
|
@@ -109,12 +110,13 @@ function excludeShebangs(comments) {
|
|
|
109
110
|
* @returns {Comment[]} lines that constitute the leading comment.
|
|
110
111
|
*/
|
|
111
112
|
function getLeadingComments(context, node) {
|
|
112
|
-
|
|
113
|
+
const all = excludeShebangs(context.sourceCode.getAllComments(node.body.length ? node.body[0] : node));
|
|
113
114
|
if (all[0].type.toLowerCase() === commentTypeOptions.block) {
|
|
114
115
|
return [all[0]];
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
let i = 1;
|
|
118
|
+
for (; i < all.length; ++i) {
|
|
119
|
+
const txt = context.sourceCode.text.slice(all[i - 1].range[1], all[i].range[0]);
|
|
118
120
|
if (!txt.match(/^(\r\n|\r|\n)$/)) {
|
|
119
121
|
break;
|
|
120
122
|
}
|
|
@@ -123,40 +125,55 @@ function getLeadingComments(context, node) {
|
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/**
|
|
126
|
-
* Generate a comment including trailing spaces out of a number of comment body
|
|
128
|
+
* Generate a comment including trailing spaces out of a number of comment body
|
|
129
|
+
* lines.
|
|
127
130
|
* @param {'block' | 'line'} commentType the type of comment to generate.
|
|
128
131
|
* @param {string[]} textArray list of lines of the comment content.
|
|
129
132
|
* @param {'\n' | '\r\n'} eol end-of-line characters.
|
|
130
|
-
* @param {number} numNewlines number of trailing lines after the comment.
|
|
131
133
|
* @returns {string} resulting comment.
|
|
132
134
|
*/
|
|
133
|
-
function genCommentBody(commentType, textArray, eol
|
|
134
|
-
var eols = eol.repeat(numNewlines);
|
|
135
|
+
function genCommentBody(commentType, textArray, eol) {
|
|
135
136
|
if (commentType === commentTypeOptions.block) {
|
|
136
|
-
return "/*" + textArray.join(eol) + "*/"
|
|
137
|
+
return "/*" + textArray.join(eol) + "*/";
|
|
137
138
|
} else {
|
|
138
|
-
|
|
139
|
+
// We need one trailing EOL on line comments to ensure the fixed source
|
|
140
|
+
// is parsable.
|
|
141
|
+
return "//" + textArray.join(eol + "//");
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
146
|
+
* Determines the start and end position in the source code of the leading
|
|
147
|
+
* comment.
|
|
145
148
|
* @param {string[]} comments list of comments.
|
|
146
|
-
* @param {'\n' | '\r\n'} eol end-of-line characters
|
|
147
149
|
* @returns {[number, number]} resulting range.
|
|
148
150
|
*/
|
|
149
|
-
function genCommentsRange(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const sourceCode = context.getSourceCode().text;
|
|
153
|
-
const headerTrailingChars = sourceCode.substring(end, end + eol.length);
|
|
154
|
-
if (headerTrailingChars === eol) {
|
|
155
|
-
end += eol.length;
|
|
156
|
-
}
|
|
151
|
+
function genCommentsRange(comments) {
|
|
152
|
+
const start = comments[0].range[0];
|
|
153
|
+
const end = comments.slice(-1)[0].range[1];
|
|
157
154
|
return [start, end];
|
|
158
155
|
}
|
|
159
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Calculates the number of leading empty lines in the source code. The function
|
|
159
|
+
* counts both Windows and POSIX line endings.
|
|
160
|
+
* @param {string} src the source code to traverse.
|
|
161
|
+
* @returns {number} the number of leading empty lines.
|
|
162
|
+
*/
|
|
163
|
+
function leadingEmptyLines(src) {
|
|
164
|
+
let numLines = 0;
|
|
165
|
+
while (true) {
|
|
166
|
+
const m = src.match(/^(\r\n|\n)/);
|
|
167
|
+
if (!m) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
assert.strictEqual(m.index, 0);
|
|
171
|
+
numLines++;
|
|
172
|
+
src = src.slice(m.index + m[0].length);
|
|
173
|
+
}
|
|
174
|
+
return numLines;
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
/**
|
|
161
178
|
* Factory for fixer that adds a missing header.
|
|
162
179
|
* @param {'block' | 'line'} commentType type of comment to use.
|
|
@@ -168,38 +185,65 @@ function genCommentsRange(context, comments, eol) {
|
|
|
168
185
|
*/
|
|
169
186
|
function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
|
|
170
187
|
return function(fixer) {
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
let insertPos = 0;
|
|
189
|
+
let newHeader = genCommentBody(commentType, headerLines, eol, numNewlines);
|
|
190
|
+
if (context.sourceCode.text.startsWith("#!")) {
|
|
173
191
|
const firstNewLinePos = context.sourceCode.text.indexOf("\n");
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
);
|
|
179
|
-
} else {
|
|
180
|
-
return fixer.insertTextBeforeRange(
|
|
181
|
-
[0, 0 /* don't care */],
|
|
182
|
-
newHeader
|
|
183
|
-
);
|
|
192
|
+
insertPos = firstNewLinePos === -1 ? context.sourceCode.text.length : firstNewLinePos + 1;
|
|
193
|
+
if (firstNewLinePos === -1) {
|
|
194
|
+
newHeader = eol + newHeader;
|
|
195
|
+
}
|
|
184
196
|
}
|
|
197
|
+
const numEmptyLines = leadingEmptyLines(context.sourceCode.text.substring(insertPos));
|
|
198
|
+
const additionalEmptyLines = Math.max(0, numNewlines - numEmptyLines);
|
|
199
|
+
newHeader += eol.repeat(additionalEmptyLines);
|
|
200
|
+
return fixer.insertTextBeforeRange(
|
|
201
|
+
[insertPos, insertPos /* don't care */],
|
|
202
|
+
newHeader
|
|
203
|
+
);
|
|
185
204
|
};
|
|
186
205
|
}
|
|
187
206
|
|
|
188
207
|
/**
|
|
189
208
|
* Factory for fixer that replaces an incorrect header.
|
|
190
209
|
* @param {'block' | 'line'} commentType type of comment to use.
|
|
191
|
-
* @param {RuleContext} context ESLint
|
|
210
|
+
* @param {RuleContext} context ESLint execution context.
|
|
192
211
|
* @param {Comment[]} leadingComments comment elements to replace.
|
|
193
212
|
* @param {string[]} headerLines lines of the header comment.
|
|
194
213
|
* @param {'\n' | '\r\n'} eol end-of-line characters
|
|
195
214
|
* @param {number} numNewlines number of trailing lines after the comment.
|
|
196
|
-
* @returns {
|
|
215
|
+
* @returns {
|
|
216
|
+
* (fixer: RuleTextEditor) => RuleTextEdit | RuleTextEdit[] | null
|
|
217
|
+
* } the fixer.
|
|
197
218
|
*/
|
|
198
219
|
function genReplaceFixer(commentType, context, leadingComments, headerLines, eol, numNewlines) {
|
|
199
220
|
return function(fixer) {
|
|
221
|
+
const commentRange = genCommentsRange(leadingComments);
|
|
222
|
+
const emptyLines = leadingEmptyLines(context.sourceCode.text.substring(commentRange[1]));
|
|
223
|
+
const missingNewlines = Math.max(0, numNewlines - emptyLines);
|
|
224
|
+
const eols = eol.repeat(missingNewlines);
|
|
200
225
|
return fixer.replaceTextRange(
|
|
201
|
-
|
|
202
|
-
genCommentBody(commentType, headerLines, eol, numNewlines)
|
|
226
|
+
commentRange,
|
|
227
|
+
genCommentBody(commentType, headerLines, eol, numNewlines) + eols
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Factory for fixer that replaces an incorrect header.
|
|
234
|
+
* @param {Comment[]} leadingComments comment elements to replace.
|
|
235
|
+
* @param {'\n' | '\r\n'} eol end-of-line characters
|
|
236
|
+
* @param {number} missingEmptyLinesCount number of trailing lines after the
|
|
237
|
+
* comment.
|
|
238
|
+
* @returns {
|
|
239
|
+
* (fixer: RuleTextEditor) => RuleTextEdit | RuleTextEdit[] | null
|
|
240
|
+
* } the fixer.
|
|
241
|
+
*/
|
|
242
|
+
function genEmptyLinesFixer(leadingComments, eol, missingEmptyLinesCount) {
|
|
243
|
+
return function(fixer) {
|
|
244
|
+
return fixer.insertTextAfterRange(
|
|
245
|
+
genCommentsRange(leadingComments),
|
|
246
|
+
eol.repeat(missingEmptyLinesCount)
|
|
203
247
|
);
|
|
204
248
|
};
|
|
205
249
|
}
|
|
@@ -207,10 +251,11 @@ function genReplaceFixer(commentType, context, leadingComments, headerLines, eol
|
|
|
207
251
|
/**
|
|
208
252
|
* Finds the option parameter within the list of rule config options.
|
|
209
253
|
* @param {HeaderOptions} options the config options passed to the rule.
|
|
210
|
-
* @returns {HeaderSettings | null} the settings parameter or `null` if no such
|
|
254
|
+
* @returns {HeaderSettings | null} the settings parameter or `null` if no such
|
|
255
|
+
* is defined.
|
|
211
256
|
*/
|
|
212
257
|
function findSettings(options) {
|
|
213
|
-
|
|
258
|
+
const lastOption = options[options.length - 1];
|
|
214
259
|
if (typeof lastOption === "object" && !Array.isArray(lastOption) && lastOption !== null
|
|
215
260
|
&& !Object.prototype.hasOwnProperty.call(lastOption, "pattern")) {
|
|
216
261
|
return lastOption;
|
|
@@ -219,12 +264,14 @@ function findSettings(options) {
|
|
|
219
264
|
}
|
|
220
265
|
|
|
221
266
|
/**
|
|
222
|
-
* Returns the used line-termination characters per the rule's config if any or
|
|
267
|
+
* Returns the used line-termination characters per the rule's config if any or
|
|
268
|
+
* else based on the runtime environments.
|
|
223
269
|
* @param {HeaderOptions} options rule configuration.
|
|
224
|
-
* @returns {'\n' | '\r\n'} the correct line ending characters for the
|
|
270
|
+
* @returns {'\n' | '\r\n'} the correct line ending characters for the
|
|
271
|
+
* environment.
|
|
225
272
|
*/
|
|
226
273
|
function getEOL(options) {
|
|
227
|
-
|
|
274
|
+
const settings = findSettings(options);
|
|
228
275
|
if (settings) {
|
|
229
276
|
if (settings.lineEndings === lineEndingOptions.unix) {
|
|
230
277
|
return "\n";
|
|
@@ -237,132 +284,62 @@ function getEOL(options) {
|
|
|
237
284
|
}
|
|
238
285
|
|
|
239
286
|
/**
|
|
240
|
-
* Tests if the first line in the source code (after a Unix she-bang) is a
|
|
287
|
+
* Tests if the first line in the source code (after a Unix she-bang) is a
|
|
288
|
+
* comment. Does not tolerate empty lines before the first match.
|
|
241
289
|
* @param {string} src source code to test.
|
|
242
290
|
* @returns {boolean} `true` if there is a comment or `false` otherwise.
|
|
243
291
|
*/
|
|
244
|
-
// TODO: check if it is valid to have the copyright notice separated by an empty line from the shebang.
|
|
245
292
|
function hasHeader(src) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (m) {
|
|
249
|
-
src = src.slice(m.index + m[0].length);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return src.substr(0, 2) === "/*" || src.substr(0, 2) === "//";
|
|
293
|
+
const srcWithoutShebang = src.replace(/^#![^\n]*\r?\n/, "");
|
|
294
|
+
return srcWithoutShebang.startsWith("/*") || srcWithoutShebang.startsWith("//");
|
|
253
295
|
}
|
|
254
296
|
|
|
255
297
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
298
|
+
* Calculates the source location of the violation that not enough empty lines
|
|
299
|
+
* follow the header.
|
|
300
|
+
* The behavior chosen is that the violation is shown over the empty (but
|
|
301
|
+
* insufficient) lines that trail the comment. A special case is when there are
|
|
302
|
+
* no empty lines after the header in which case we highlight the next character
|
|
303
|
+
* in the source regardless of which one it is).
|
|
304
|
+
* @param {Comment[]} leadingComments the comment lines that constitute the
|
|
305
|
+
* header.
|
|
306
|
+
* @param {number} actualEmptyLines the number of empty lines that follow the
|
|
307
|
+
* header.
|
|
308
|
+
* @returns {SourceLocation} the location (line and column) of the violation.
|
|
260
309
|
*/
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
310
|
+
function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
|
|
311
|
+
const lastCommentLineLocEnd = leadingComments[leadingComments.length - 1].loc.end;
|
|
312
|
+
return {
|
|
313
|
+
start: lastCommentLineLocEnd,
|
|
314
|
+
end: {
|
|
315
|
+
column: actualEmptyLines === 0 ? lastCommentLineLocEnd.column + 1 : 0,
|
|
316
|
+
line: lastCommentLineLocEnd.line + actualEmptyLines
|
|
268
317
|
}
|
|
269
|
-
}
|
|
270
|
-
return true;
|
|
318
|
+
};
|
|
271
319
|
}
|
|
272
320
|
|
|
273
321
|
module.exports = {
|
|
274
322
|
meta: {
|
|
275
323
|
type: "layout",
|
|
324
|
+
docs: {
|
|
325
|
+
description,
|
|
326
|
+
recommended,
|
|
327
|
+
url
|
|
328
|
+
},
|
|
276
329
|
fixable: "whitespace",
|
|
277
|
-
schema
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
type: "object",
|
|
291
|
-
properties: {
|
|
292
|
-
pattern: {
|
|
293
|
-
type: "string"
|
|
294
|
-
},
|
|
295
|
-
template: {
|
|
296
|
-
type: "string"
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
required: ["pattern"],
|
|
300
|
-
additionalProperties: false
|
|
301
|
-
}
|
|
302
|
-
]
|
|
303
|
-
},
|
|
304
|
-
headerLines: {
|
|
305
|
-
anyOf: [
|
|
306
|
-
{
|
|
307
|
-
$ref: "#/definitions/line"
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
type: "array",
|
|
311
|
-
items: {
|
|
312
|
-
$ref: "#/definitions/line"
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
]
|
|
316
|
-
},
|
|
317
|
-
numNewlines: {
|
|
318
|
-
type: "integer",
|
|
319
|
-
minimum: 0
|
|
320
|
-
},
|
|
321
|
-
settings: {
|
|
322
|
-
type: "object",
|
|
323
|
-
properties: {
|
|
324
|
-
lineEndings: {
|
|
325
|
-
type: "string",
|
|
326
|
-
enum: [lineEndingOptions.unix, lineEndingOptions.windows]
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
additionalProperties: false
|
|
330
|
-
},
|
|
331
|
-
options: {
|
|
332
|
-
anyOf: [
|
|
333
|
-
{
|
|
334
|
-
type: "array",
|
|
335
|
-
minItems: 1,
|
|
336
|
-
maxItems: 2,
|
|
337
|
-
items: [
|
|
338
|
-
{ type: "string" },
|
|
339
|
-
{ $ref: "#/definitions/settings" }
|
|
340
|
-
]
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
type: "array",
|
|
344
|
-
minItems: 2,
|
|
345
|
-
maxItems: 3,
|
|
346
|
-
items: [
|
|
347
|
-
{ $ref: "#/definitions/commentType" },
|
|
348
|
-
{ $ref: "#/definitions/headerLines" },
|
|
349
|
-
{ $ref: "#/definitions/settings" }
|
|
350
|
-
]
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
type: "array",
|
|
354
|
-
minItems: 3,
|
|
355
|
-
maxItems: 4,
|
|
356
|
-
items: [
|
|
357
|
-
{ $ref: "#/definitions/commentType" },
|
|
358
|
-
{ $ref: "#/definitions/headerLines" },
|
|
359
|
-
{ $ref: "#/definitions/numNewlines" },
|
|
360
|
-
{ $ref: "#/definitions/settings" }
|
|
361
|
-
]
|
|
362
|
-
}
|
|
363
|
-
]
|
|
364
|
-
}
|
|
365
|
-
}
|
|
330
|
+
schema,
|
|
331
|
+
defaultOptions: [{}],
|
|
332
|
+
messages: {
|
|
333
|
+
headerLineMismatchAtPos: "header line does not match expected after this position; expected: {{expected}}",
|
|
334
|
+
headerLineTooLong: "header line longer than expected",
|
|
335
|
+
headerLineTooShort: "header line shorter than expected; missing: {{remainder}}",
|
|
336
|
+
headerTooShort: "header too short: missing lines: {{remainder}}",
|
|
337
|
+
headerTooLong: "header too long",
|
|
338
|
+
incorrectCommentType: "header should be a {{commentType}} comment",
|
|
339
|
+
incorrectHeader: "incorrect header",
|
|
340
|
+
incorrectHeaderLine: "header line does not match pattern: {{pattern}}",
|
|
341
|
+
missingHeader: "missing header",
|
|
342
|
+
noNewlineAfterHeader: "not enough newlines after header: expected: {{expected}}, actual: {{actual}}"
|
|
366
343
|
}
|
|
367
344
|
},
|
|
368
345
|
/**
|
|
@@ -371,26 +348,28 @@ module.exports = {
|
|
|
371
348
|
* @returns {NodeListener} the rule definition.
|
|
372
349
|
*/
|
|
373
350
|
create: function(context) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
351
|
+
let options = context.options;
|
|
352
|
+
const numNewlines = options.length > 2 && typeof options[2] === "number" ? options[2] : 1;
|
|
353
|
+
const eol = getEOL(options);
|
|
377
354
|
|
|
378
355
|
// If just one option then read comment from file
|
|
379
356
|
if (options.length === 1 || (options.length === 2 && findSettings(options))) {
|
|
380
|
-
|
|
357
|
+
const text = fs.readFileSync(context.options[0], "utf8");
|
|
381
358
|
options = commentParser(text);
|
|
382
359
|
}
|
|
383
360
|
|
|
384
|
-
|
|
385
|
-
|
|
361
|
+
const commentType = options[0].toLowerCase();
|
|
362
|
+
/** @type {(string | RegExp)[]} */
|
|
363
|
+
let headerLines;
|
|
364
|
+
let fixLines = [];
|
|
386
365
|
// If any of the lines are regular expressions, then we can't
|
|
387
366
|
// automatically fix them. We set this to true below once we
|
|
388
367
|
// ensure none of the lines are of type RegExp
|
|
389
|
-
|
|
368
|
+
let canFix = false;
|
|
390
369
|
if (Array.isArray(options[1])) {
|
|
391
370
|
canFix = true;
|
|
392
371
|
headerLines = options[1].map(function(line) {
|
|
393
|
-
|
|
372
|
+
const isRegex = isPattern(line);
|
|
394
373
|
// Can only fix regex option if a template is also provided
|
|
395
374
|
if (isRegex && !line.template) {
|
|
396
375
|
canFix = false;
|
|
@@ -399,7 +378,7 @@ module.exports = {
|
|
|
399
378
|
return isRegex ? new RegExp(line.pattern) : line;
|
|
400
379
|
});
|
|
401
380
|
} else if (isPattern(options[1])) {
|
|
402
|
-
|
|
381
|
+
const line = options[1];
|
|
403
382
|
headerLines = [new RegExp(line.pattern)];
|
|
404
383
|
fixLines.push(line.template || line);
|
|
405
384
|
// Same as above for regex and template
|
|
@@ -411,114 +390,346 @@ module.exports = {
|
|
|
411
390
|
}
|
|
412
391
|
|
|
413
392
|
return {
|
|
393
|
+
/**
|
|
394
|
+
* Hooks into the processing of the overall script node to do the
|
|
395
|
+
* header validation.
|
|
396
|
+
* @param {Program} node the whole script node
|
|
397
|
+
* @returns {void}
|
|
398
|
+
*/
|
|
414
399
|
Program: function(node) {
|
|
415
|
-
if (!hasHeader(context.sourceCode.
|
|
400
|
+
if (!hasHeader(context.sourceCode.text)) {
|
|
401
|
+
const hasShebang = context.sourceCode.text.startsWith("#!");
|
|
402
|
+
const line = hasShebang ? 2 : 1;
|
|
416
403
|
context.report({
|
|
417
|
-
loc:
|
|
418
|
-
|
|
404
|
+
loc: {
|
|
405
|
+
start: {
|
|
406
|
+
column: 1,
|
|
407
|
+
line
|
|
408
|
+
},
|
|
409
|
+
end: {
|
|
410
|
+
column: 1,
|
|
411
|
+
line
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
messageId: "missingHeader",
|
|
419
415
|
fix: genPrependFixer(commentType, context, fixLines, eol, numNewlines)
|
|
420
416
|
});
|
|
421
|
-
|
|
422
|
-
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const leadingComments = getLeadingComments(context, node);
|
|
423
420
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
421
|
+
if (leadingComments[0].type.toLowerCase() !== commentType) {
|
|
422
|
+
context.report({
|
|
423
|
+
loc: {
|
|
424
|
+
start: leadingComments[0].loc.start,
|
|
425
|
+
end: leadingComments[leadingComments.length - 1].loc.end
|
|
426
|
+
},
|
|
427
|
+
messageId: "incorrectCommentType",
|
|
428
|
+
data: {
|
|
429
|
+
commentType: commentType
|
|
430
|
+
},
|
|
431
|
+
fix: canFix
|
|
432
|
+
? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
|
|
433
|
+
: null
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (commentType === commentTypeOptions.line) {
|
|
438
|
+
if (headerLines.length === 1) {
|
|
439
|
+
const leadingCommentValues = leadingComments.map((c) => c.value);
|
|
440
|
+
if (
|
|
441
|
+
!match(leadingCommentValues.join("\n"), headerLines[0])
|
|
442
|
+
&& !match(leadingCommentValues.join("\r\n"), headerLines[0])
|
|
443
|
+
) {
|
|
444
|
+
context.report({
|
|
445
|
+
loc: {
|
|
446
|
+
start: leadingComments[0].loc.start,
|
|
447
|
+
end: leadingComments[leadingComments.length - 1].loc.end
|
|
448
|
+
},
|
|
449
|
+
messageId: "incorrectHeader",
|
|
450
|
+
fix: canFix
|
|
451
|
+
? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
|
|
452
|
+
: null
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
439
456
|
} else {
|
|
440
|
-
|
|
441
|
-
if (leadingComments.length <
|
|
457
|
+
for (let i = 0; i < headerLines.length; i++) {
|
|
458
|
+
if (leadingComments.length - 1 < i) {
|
|
442
459
|
context.report({
|
|
443
|
-
loc:
|
|
444
|
-
|
|
445
|
-
|
|
460
|
+
loc: {
|
|
461
|
+
start: leadingComments[leadingComments.length - 1].loc.end
|
|
462
|
+
},
|
|
463
|
+
messageId: "headerTooShort",
|
|
464
|
+
data: {
|
|
465
|
+
remainder: headerLines.slice(i).join(eol)
|
|
466
|
+
},
|
|
467
|
+
fix: canFix
|
|
468
|
+
? genReplaceFixer(
|
|
469
|
+
commentType,
|
|
470
|
+
context,
|
|
471
|
+
leadingComments,
|
|
472
|
+
fixLines,
|
|
473
|
+
eol,
|
|
474
|
+
numNewlines)
|
|
475
|
+
: null
|
|
446
476
|
});
|
|
447
477
|
return;
|
|
448
478
|
}
|
|
449
|
-
if (headerLines
|
|
450
|
-
const
|
|
451
|
-
|
|
479
|
+
if (typeof headerLines[i] === "string") {
|
|
480
|
+
const leadingCommentLength = leadingComments[i].value.length;
|
|
481
|
+
const headerLineLength = headerLines[i].length;
|
|
482
|
+
for (let j = 0; j < Math.min(leadingCommentLength, headerLineLength); j++) {
|
|
483
|
+
if (leadingComments[i].value[j] !== headerLines[i][j]) {
|
|
484
|
+
context.report({
|
|
485
|
+
loc: {
|
|
486
|
+
start: {
|
|
487
|
+
column: "//".length + j,
|
|
488
|
+
line: leadingComments[i].loc.start.line
|
|
489
|
+
},
|
|
490
|
+
end: leadingComments[i].loc.end
|
|
491
|
+
},
|
|
492
|
+
messageId: "headerLineMismatchAtPos",
|
|
493
|
+
data: {
|
|
494
|
+
expected: headerLines[i].substring(j)
|
|
495
|
+
},
|
|
496
|
+
fix: genReplaceFixer(
|
|
497
|
+
commentType,
|
|
498
|
+
context,
|
|
499
|
+
leadingComments,
|
|
500
|
+
fixLines,
|
|
501
|
+
eol,
|
|
502
|
+
numNewlines)
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (leadingCommentLength < headerLineLength) {
|
|
508
|
+
context.report({
|
|
509
|
+
loc: {
|
|
510
|
+
start: leadingComments[i].loc.end,
|
|
511
|
+
},
|
|
512
|
+
messageId: "headerLineTooShort",
|
|
513
|
+
data: {
|
|
514
|
+
remainder: headerLines[i].substring(leadingCommentLength)
|
|
515
|
+
},
|
|
516
|
+
fix: canFix
|
|
517
|
+
? genReplaceFixer(
|
|
518
|
+
commentType,
|
|
519
|
+
context,
|
|
520
|
+
leadingComments,
|
|
521
|
+
fixLines,
|
|
522
|
+
eol,
|
|
523
|
+
numNewlines)
|
|
524
|
+
: null
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (leadingCommentLength > headerLineLength) {
|
|
452
529
|
context.report({
|
|
453
|
-
loc:
|
|
454
|
-
|
|
455
|
-
|
|
530
|
+
loc: {
|
|
531
|
+
start: {
|
|
532
|
+
column: "//".length + headerLineLength,
|
|
533
|
+
line: leadingComments[i].loc.start.line
|
|
534
|
+
},
|
|
535
|
+
end: leadingComments[i].loc.end,
|
|
536
|
+
},
|
|
537
|
+
messageId: "headerLineTooLong",
|
|
538
|
+
fix: canFix
|
|
539
|
+
? genReplaceFixer(
|
|
540
|
+
commentType,
|
|
541
|
+
context,
|
|
542
|
+
leadingComments,
|
|
543
|
+
fixLines,
|
|
544
|
+
eol,
|
|
545
|
+
numNewlines)
|
|
546
|
+
: null
|
|
456
547
|
});
|
|
457
548
|
return;
|
|
458
549
|
}
|
|
459
550
|
} else {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
551
|
+
if (!match(leadingComments[i].value, headerLines[i])) {
|
|
552
|
+
context.report({
|
|
553
|
+
loc: {
|
|
554
|
+
start: {
|
|
555
|
+
column: "//".length,
|
|
556
|
+
line: leadingComments[i].loc.start.line,
|
|
557
|
+
},
|
|
558
|
+
end: leadingComments[i].loc.end,
|
|
559
|
+
},
|
|
560
|
+
messageId: "incorrectHeaderLine",
|
|
561
|
+
data: {
|
|
562
|
+
pattern: headerLines[i]
|
|
563
|
+
},
|
|
564
|
+
fix: canFix
|
|
565
|
+
? genReplaceFixer(
|
|
566
|
+
commentType,
|
|
567
|
+
context,
|
|
568
|
+
leadingComments,
|
|
569
|
+
fixLines,
|
|
570
|
+
eol,
|
|
571
|
+
numNewlines)
|
|
572
|
+
: null
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
469
575
|
}
|
|
470
576
|
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
471
579
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
580
|
+
const actualLeadingEmptyLines = leadingEmptyLines(
|
|
581
|
+
context.sourceCode.text.substring(leadingComments[headerLines.length - 1].range[1]));
|
|
582
|
+
const missingEmptyLines = numNewlines - actualLeadingEmptyLines;
|
|
583
|
+
if (missingEmptyLines > 0) {
|
|
584
|
+
context.report({
|
|
585
|
+
loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
|
|
586
|
+
messageId: "noNewlineAfterHeader",
|
|
587
|
+
data: {
|
|
588
|
+
expected: numNewlines,
|
|
589
|
+
actual: actualLeadingEmptyLines
|
|
590
|
+
},
|
|
591
|
+
fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// if block comment pattern has more than 1 line, we also split
|
|
597
|
+
// the comment
|
|
598
|
+
let leadingLines = [leadingComments[0].value];
|
|
599
|
+
if (headerLines.length > 1) {
|
|
600
|
+
leadingLines = leadingComments[0].value.split(/\r?\n/);
|
|
601
|
+
}
|
|
487
602
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
603
|
+
/** @type {null | string} */
|
|
604
|
+
let errorMessageId = null;
|
|
605
|
+
/** @type {null | Record<string, string | RegExp>} */
|
|
606
|
+
let errorMessageData = null;
|
|
607
|
+
/** @type {null | SourceLocation} */
|
|
608
|
+
let errorMessageLoc = null;
|
|
609
|
+
for (let i = 0; i < headerLines.length; i++) {
|
|
610
|
+
const leadingLine = leadingLines[i];
|
|
611
|
+
const headerLine = headerLines[i];
|
|
612
|
+
if (typeof headerLine === "string") {
|
|
613
|
+
for (let j = 0; j < Math.min(leadingLine.length, headerLine.length); j++) {
|
|
614
|
+
if (leadingLine[j] !== headerLine[j]) {
|
|
615
|
+
errorMessageId = "headerLineMismatchAtPos";
|
|
616
|
+
const columnOffset = i === 0 ? "/*".length : 0;
|
|
617
|
+
const line = leadingComments[0].loc.start.line + i;
|
|
618
|
+
errorMessageLoc = {
|
|
619
|
+
start: {
|
|
620
|
+
column: columnOffset + j,
|
|
621
|
+
line
|
|
622
|
+
},
|
|
623
|
+
end: {
|
|
624
|
+
column: columnOffset + leadingLine.length,
|
|
625
|
+
line
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
errorMessageData = {
|
|
629
|
+
expected: headerLine.substring(j)
|
|
630
|
+
};
|
|
631
|
+
break;
|
|
491
632
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
633
|
+
}
|
|
634
|
+
if (errorMessageId) {
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
if (leadingLine.length < headerLine.length) {
|
|
638
|
+
errorMessageId = "headerLineTooShort";
|
|
639
|
+
const startColumn = (i === 0 ? "/*".length : 0) + leadingLine.length;
|
|
640
|
+
errorMessageLoc = {
|
|
641
|
+
start: {
|
|
642
|
+
column: startColumn,
|
|
643
|
+
line: leadingComments[0].loc.start.line + i
|
|
644
|
+
},
|
|
645
|
+
end: {
|
|
646
|
+
column: startColumn + 1,
|
|
647
|
+
line: leadingComments[0].loc.start.line + i
|
|
498
648
|
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
649
|
+
};
|
|
650
|
+
errorMessageData = {
|
|
651
|
+
remainder: headerLines[i].substring(leadingLine.length)
|
|
652
|
+
};
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
if (leadingLine.length > headerLine.length) {
|
|
656
|
+
errorMessageId = "headerLineTooLong";
|
|
657
|
+
errorMessageLoc = {
|
|
658
|
+
start: {
|
|
659
|
+
column: (i === 0 ? "/*".length : 0) + headerLine.length,
|
|
660
|
+
line: leadingComments[0].loc.start.line + i
|
|
661
|
+
},
|
|
662
|
+
end: {
|
|
663
|
+
column: (i === 0 ? "/*".length : 0) + leadingLine.length,
|
|
664
|
+
line: leadingComments[0].loc.start.line + i
|
|
504
665
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
666
|
+
};
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
if (!match(leadingLine, headerLine)) {
|
|
671
|
+
errorMessageId = "incorrectHeaderLine";
|
|
672
|
+
errorMessageData = {
|
|
673
|
+
pattern: headerLine
|
|
674
|
+
};
|
|
675
|
+
const columnOffset = i === 0 ? "/*".length : 0;
|
|
676
|
+
errorMessageLoc = {
|
|
677
|
+
start: {
|
|
678
|
+
column: columnOffset + 0,
|
|
679
|
+
line: leadingComments[0].loc.start.line + i
|
|
680
|
+
},
|
|
681
|
+
end: {
|
|
682
|
+
column: columnOffset + leadingLine.length,
|
|
683
|
+
line: leadingComments[0].loc.start.line + i
|
|
518
684
|
}
|
|
519
|
-
}
|
|
685
|
+
};
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!errorMessageId && leadingLines.length > headerLines.length) {
|
|
692
|
+
errorMessageId = "headerTooLong";
|
|
693
|
+
errorMessageLoc = {
|
|
694
|
+
start: {
|
|
695
|
+
column: (headerLines.length === 0 ? "/*".length : 0) + 0,
|
|
696
|
+
line: leadingComments[0].loc.start.line + headerLines.length
|
|
697
|
+
},
|
|
698
|
+
end: {
|
|
699
|
+
column: leadingComments[leadingComments.length - 1].loc.end.column - "*/".length,
|
|
700
|
+
line: leadingComments[leadingComments.length - 1].loc.end.line
|
|
520
701
|
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (errorMessageId) {
|
|
706
|
+
if (canFix && headerLines.length > 1) {
|
|
707
|
+
fixLines = [fixLines.join(eol)];
|
|
521
708
|
}
|
|
709
|
+
context.report({
|
|
710
|
+
loc: errorMessageLoc,
|
|
711
|
+
messageId: errorMessageId,
|
|
712
|
+
data: errorMessageData,
|
|
713
|
+
fix: canFix
|
|
714
|
+
? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
|
|
715
|
+
: null
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const actualLeadingEmptyLines = leadingEmptyLines(
|
|
721
|
+
context.sourceCode.text.substring(leadingComments[0].range[1]));
|
|
722
|
+
const missingEmptyLines = numNewlines - actualLeadingEmptyLines;
|
|
723
|
+
if (missingEmptyLines > 0) {
|
|
724
|
+
context.report({
|
|
725
|
+
loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
|
|
726
|
+
messageId: "noNewlineAfterHeader",
|
|
727
|
+
data: {
|
|
728
|
+
expected: numNewlines,
|
|
729
|
+
actual: actualLeadingEmptyLines
|
|
730
|
+
},
|
|
731
|
+
fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
|
|
732
|
+
});
|
|
522
733
|
}
|
|
523
734
|
}
|
|
524
735
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MIT License
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025-present Tony Ganchev and contributors
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the “Software”), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in
|
|
14
|
+
* all copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
"use strict";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @enum {string}
|
|
29
|
+
*/
|
|
30
|
+
const lineEndingOptions = Object.freeze({
|
|
31
|
+
os: "os",
|
|
32
|
+
unix: "unix",
|
|
33
|
+
windows: "windows",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @enum {string}
|
|
38
|
+
*/
|
|
39
|
+
const commentTypeOptions = Object.freeze({
|
|
40
|
+
block: "block",
|
|
41
|
+
line: "line"
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const schema = Object.freeze({
|
|
45
|
+
$ref: "#/definitions/options",
|
|
46
|
+
definitions: {
|
|
47
|
+
commentType: {
|
|
48
|
+
type: "string",
|
|
49
|
+
enum: [commentTypeOptions.block, commentTypeOptions.line]
|
|
50
|
+
},
|
|
51
|
+
line: {
|
|
52
|
+
anyOf: [
|
|
53
|
+
{
|
|
54
|
+
type: "string"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
pattern: {
|
|
60
|
+
type: "string"
|
|
61
|
+
},
|
|
62
|
+
template: {
|
|
63
|
+
type: "string"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
required: ["pattern"],
|
|
67
|
+
additionalProperties: false
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
headerLines: {
|
|
72
|
+
anyOf: [
|
|
73
|
+
{
|
|
74
|
+
$ref: "#/definitions/line"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: "array",
|
|
78
|
+
items: {
|
|
79
|
+
$ref: "#/definitions/line"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
numNewlines: {
|
|
85
|
+
type: "integer",
|
|
86
|
+
minimum: 0
|
|
87
|
+
},
|
|
88
|
+
settings: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
lineEndings: {
|
|
92
|
+
type: "string",
|
|
93
|
+
enum: [lineEndingOptions.unix, lineEndingOptions.windows]
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
additionalProperties: false
|
|
97
|
+
},
|
|
98
|
+
options: {
|
|
99
|
+
anyOf: [
|
|
100
|
+
{
|
|
101
|
+
type: "array",
|
|
102
|
+
minItems: 1,
|
|
103
|
+
maxItems: 2,
|
|
104
|
+
items: [
|
|
105
|
+
{ type: "string" },
|
|
106
|
+
{ $ref: "#/definitions/settings" }
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: "array",
|
|
111
|
+
minItems: 2,
|
|
112
|
+
maxItems: 3,
|
|
113
|
+
items: [
|
|
114
|
+
{ $ref: "#/definitions/commentType" },
|
|
115
|
+
{ $ref: "#/definitions/headerLines" },
|
|
116
|
+
{ $ref: "#/definitions/settings" }
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: "array",
|
|
121
|
+
minItems: 3,
|
|
122
|
+
maxItems: 4,
|
|
123
|
+
items: [
|
|
124
|
+
{ $ref: "#/definitions/commentType" },
|
|
125
|
+
{ $ref: "#/definitions/headerLines" },
|
|
126
|
+
{ $ref: "#/definitions/numNewlines" },
|
|
127
|
+
{ $ref: "#/definitions/settings" }
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
module.exports = { lineEndingOptions, commentTypeOptions, schema };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tony.ganchev/eslint-plugin-header",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.11",
|
|
4
4
|
"description": "ESLint plugin to ensure files begin with a given comment, usually a copyright or license notice.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -8,16 +8,22 @@
|
|
|
8
8
|
"!/lib/rules/test-utils.js"
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
|
+
"eslint": "npx eslint .",
|
|
12
|
+
"lint": "npm run eslint && npm run markdownlint",
|
|
13
|
+
"markdownlint": "npx markdownlint-cli *.md",
|
|
11
14
|
"test": "npm run lint && npm run unit",
|
|
12
|
-
"unit": "nyc --reporter=html --reporter=text --reporter=text-summary --check-coverage=true --statements=
|
|
13
|
-
"lint": "eslint ."
|
|
15
|
+
"unit": "npx nyc --reporter=html --reporter=text --reporter=text-summary --reporter=lcov --check-coverage=true --statements=100 --branches=100 --lines=100 --functions=100 mocha tests/lib/*.js tests/lib/**/*.js"
|
|
14
16
|
},
|
|
15
17
|
"devDependencies": {
|
|
16
18
|
"@eslint/eslintrc": "^3.3.1",
|
|
17
|
-
"@eslint/js": "^9.
|
|
18
|
-
"eslint": "^
|
|
19
|
-
"eslint-plugin
|
|
20
|
-
"
|
|
19
|
+
"@eslint/js": "^9.39.1",
|
|
20
|
+
"@eslint/markdown": "^7.5.1",
|
|
21
|
+
"@stylistic/eslint-plugin": "^5.5.0",
|
|
22
|
+
"eslint": "^9.39.1",
|
|
23
|
+
"eslint-plugin-eslint-plugin": "^7.2.0",
|
|
24
|
+
"eslint-plugin-jsdoc": "^61.1.12",
|
|
25
|
+
"eslint-plugin-n": "^17.23.1",
|
|
26
|
+
"mocha": "^11.7.5",
|
|
21
27
|
"nyc": "^17.1.0",
|
|
22
28
|
"testdouble": "^3.20.2"
|
|
23
29
|
},
|