@tony.ganchev/eslint-plugin-header 3.1.9 → 3.1.10

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 CHANGED
@@ -1,3 +1,5 @@
1
+ # MIT License
2
+
1
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
@@ -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 all
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,
@@ -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 all
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 of content lines.
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 content broken into lines.
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.substr(0, 2) === "//") {
43
+ if (text.startsWith("//")) {
39
44
  return [
40
45
  "line",
41
- text.split(/\r?\n/).map(function(line) {
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("Could not parse comment file: the file must contain either just line comments (//) or a single block comment (/* ... */)");
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;
@@ -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 all
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,
@@ -37,48 +37,46 @@
37
37
  * @typedef {import('eslint').Rule.RuleContext} RuleContext
38
38
  */
39
39
 
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
40
  /**
57
41
  * Local type defintions.
58
42
  * @typedef {string | { pattern: string, template?: string }} HeaderLine
59
43
  * @typedef {(HeaderLine | HeaderLine[])} HeaderLines
60
44
  * @typedef {{ lineEndings: ('unix' | 'windows') }} HeaderSettings
61
- * @typedef {([string] | [string, HeaderSettings] | [('block' | 'line') | HeaderLines ] | [('block' | 'line') | HeaderLines | HeaderSettings] | [('block' | 'line') | HeaderLines | number ] | [('block' | 'line') | HeaderLines | number | HeaderSettings])} HeaderOptions
45
+ * @typedef {
46
+ * [string]
47
+ * | [string, HeaderSettings]
48
+ * | [('block' | 'line') | HeaderLines ]
49
+ * | [('block' | 'line') | HeaderLines | HeaderSettings]
50
+ * | [('block' | 'line') | HeaderLines | number ]
51
+ * | [('block' | 'line') | HeaderLines | number | HeaderSettings]
52
+ * } HeaderOptions
62
53
  */
63
54
 
64
- var fs = require("fs");
65
- var commentParser = require("../comment-parser");
66
- var os = require("os");
55
+ const assert = require("assert");
56
+ const fs = require("fs");
57
+ const os = require("os");
58
+ const commentParser = require("../comment-parser");
59
+ const { description, recommended, url } = require("./header.docs");
60
+ const { commentTypeOptions, lineEndingOptions, schema } = require("./header.schema");
67
61
 
68
62
  /**
69
- * Tests if the passed line configuration string or object is a pattern definition.
63
+ * Tests if the passed line configuration string or object is a pattern
64
+ * definition.
70
65
  * @param {HeaderLine} object line configuration object or string
71
- * @returns {boolean} `true` if the line configuration is a pattern-dfining object or `false` otherwise.
66
+ * @returns {boolean} `true` if the line configuration is a pattern-defining
67
+ * object or `false` otherwise.
72
68
  */
73
69
  function isPattern(object) {
74
70
  return typeof object === "object" && Object.prototype.hasOwnProperty.call(object, "pattern");
75
71
  }
76
72
 
77
73
  /**
78
- * Utility over a line config argument to match an expected string either against a regex or for full match against a string.
74
+ * Utility over a line config argument to match an expected string either
75
+ * against a regex or for full match against a string.
79
76
  * @param {HeaderLine} actual the string to test.
80
77
  * @param {string} expected The string or regex to test again.
81
- * @returns {boolean} `true` if the passed string matches the expected line config or `false` otherwise.
78
+ * @returns {boolean} `true` if the passed string matches the expected line
79
+ * config or `false` otherwise.
82
80
  */
83
81
  function match(actual, expected) {
84
82
  if (expected.test) {
@@ -91,7 +89,9 @@ function match(actual, expected) {
91
89
  /**
92
90
  * Remove Unix she-bangs from the list of comments.
93
91
  * @param {Comment[]} comments the list of comment lines.
94
- * @returns {Comment[]} the list of comments with containing all incomming comments from `comments` with the shebang comments omitted.
92
+ * @returns {Comment[]} the list of comments with containing all incomming
93
+ * comments from `comments` with the shebang
94
+ * comments omitted.
95
95
  */
96
96
  function excludeShebangs(comments) {
97
97
  return comments.filter(function(comment) {
@@ -109,12 +109,13 @@ function excludeShebangs(comments) {
109
109
  * @returns {Comment[]} lines that constitute the leading comment.
110
110
  */
111
111
  function getLeadingComments(context, node) {
112
- var all = excludeShebangs(context.getSourceCode().getAllComments(node.body.length ? node.body[0] : node));
112
+ const all = excludeShebangs(context.sourceCode.getAllComments(node.body.length ? node.body[0] : node));
113
113
  if (all[0].type.toLowerCase() === commentTypeOptions.block) {
114
114
  return [all[0]];
115
115
  }
116
- for (var i = 1; i < all.length; ++i) {
117
- var txt = context.getSourceCode().getText().slice(all[i - 1].range[1], all[i].range[0]);
116
+ let i = 1;
117
+ for (; i < all.length; ++i) {
118
+ const txt = context.sourceCode.text.slice(all[i - 1].range[1], all[i].range[0]);
118
119
  if (!txt.match(/^(\r\n|\r|\n)$/)) {
119
120
  break;
120
121
  }
@@ -123,40 +124,55 @@ function getLeadingComments(context, node) {
123
124
  }
124
125
 
125
126
  /**
126
- * Generate a comment including trailing spaces out of a number of comment body lines.
127
+ * Generate a comment including trailing spaces out of a number of comment body
128
+ * lines.
127
129
  * @param {'block' | 'line'} commentType the type of comment to generate.
128
130
  * @param {string[]} textArray list of lines of the comment content.
129
131
  * @param {'\n' | '\r\n'} eol end-of-line characters.
130
- * @param {number} numNewlines number of trailing lines after the comment.
131
132
  * @returns {string} resulting comment.
132
133
  */
133
- function genCommentBody(commentType, textArray, eol, numNewlines) {
134
- var eols = eol.repeat(numNewlines);
134
+ function genCommentBody(commentType, textArray, eol) {
135
135
  if (commentType === commentTypeOptions.block) {
136
- return "/*" + textArray.join(eol) + "*/" + eols;
136
+ return "/*" + textArray.join(eol) + "*/";
137
137
  } else {
138
- return "//" + textArray.join(eol + "//") + eols;
138
+ // We need one trailing EOL on line comments to ensure the fixed source
139
+ // is parsable.
140
+ return "//" + textArray.join(eol + "//");
139
141
  }
140
142
  }
141
143
 
142
144
  /**
143
- * ...
144
- * @param {RuleContext} context ESLint rule execution context.
145
+ * Determines the start and end position in the source code of the leading
146
+ * comment.
145
147
  * @param {string[]} comments list of comments.
146
- * @param {'\n' | '\r\n'} eol end-of-line characters
147
148
  * @returns {[number, number]} resulting range.
148
149
  */
149
- function genCommentsRange(context, comments, eol) {
150
- var start = comments[0].range[0];
151
- var end = comments.slice(-1)[0].range[1];
152
- const sourceCode = context.getSourceCode().text;
153
- const headerTrailingChars = sourceCode.substring(end, end + eol.length);
154
- if (headerTrailingChars === eol) {
155
- end += eol.length;
156
- }
150
+ function genCommentsRange(comments) {
151
+ const start = comments[0].range[0];
152
+ const end = comments.slice(-1)[0].range[1];
157
153
  return [start, end];
158
154
  }
159
155
 
156
+ /**
157
+ * Calculates the number of leading empty lines in the source code. The function
158
+ * counts both Windows and POSIX line endings.
159
+ * @param {string} src the source code to traverse.
160
+ * @returns {number} the number of leading empty lines.
161
+ */
162
+ function leadingEmptyLines(src) {
163
+ let numLines = 0;
164
+ while (true) {
165
+ const m = src.match(/^(\r\n|\n)/);
166
+ if (!m) {
167
+ break;
168
+ }
169
+ assert.strictEqual(m.index, 0);
170
+ numLines++;
171
+ src = src.slice(m.index + m[0].length);
172
+ }
173
+ return numLines;
174
+ }
175
+
160
176
  /**
161
177
  * Factory for fixer that adds a missing header.
162
178
  * @param {'block' | 'line'} commentType type of comment to use.
@@ -168,38 +184,65 @@ function genCommentsRange(context, comments, eol) {
168
184
  */
169
185
  function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
170
186
  return function(fixer) {
171
- const newHeader = genCommentBody(commentType, headerLines, eol, numNewlines);
187
+ let insertPos = 0;
188
+ let newHeader = genCommentBody(commentType, headerLines, eol, numNewlines);
172
189
  if (context.sourceCode.text.substring(0, 2) === "#!") {
173
190
  const firstNewLinePos = context.sourceCode.text.indexOf("\n");
174
- const insertPos = firstNewLinePos === -1 ? context.sourceCode.text.length : firstNewLinePos + 1;
175
- return fixer.insertTextBeforeRange(
176
- [insertPos, insertPos /* don't care */],
177
- (firstNewLinePos === -1 ? eol : "") + newHeader
178
- );
179
- } else {
180
- return fixer.insertTextBeforeRange(
181
- [0, 0 /* don't care */],
182
- newHeader
183
- );
191
+ insertPos = firstNewLinePos === -1 ? context.sourceCode.text.length : firstNewLinePos + 1;
192
+ if (firstNewLinePos === -1) {
193
+ newHeader = eol + newHeader;
194
+ }
184
195
  }
196
+ const numEmptyLines = leadingEmptyLines(context.sourceCode.text.substring(insertPos));
197
+ const additionalEmptyLines = Math.max(0, numNewlines - numEmptyLines);
198
+ newHeader += eol.repeat(additionalEmptyLines);
199
+ return fixer.insertTextBeforeRange(
200
+ [insertPos, insertPos /* don't care */],
201
+ newHeader
202
+ );
185
203
  };
186
204
  }
187
205
 
188
206
  /**
189
207
  * Factory for fixer that replaces an incorrect header.
190
208
  * @param {'block' | 'line'} commentType type of comment to use.
191
- * @param {RuleContext} context ESLint rule execution context.
209
+ * @param {RuleContext} context ESLint execution context.
192
210
  * @param {Comment[]} leadingComments comment elements to replace.
193
211
  * @param {string[]} headerLines lines of the header comment.
194
212
  * @param {'\n' | '\r\n'} eol end-of-line characters
195
213
  * @param {number} numNewlines number of trailing lines after the comment.
196
- * @returns {(fixer: RuleTextEditor) => RuleTextEdit | RuleTextEdit[] | null} the fixer.
214
+ * @returns {
215
+ * (fixer: RuleTextEditor) => RuleTextEdit | RuleTextEdit[] | null
216
+ * } the fixer.
197
217
  */
198
218
  function genReplaceFixer(commentType, context, leadingComments, headerLines, eol, numNewlines) {
199
219
  return function(fixer) {
220
+ const commentRange = genCommentsRange(leadingComments);
221
+ const emptyLines = leadingEmptyLines(context.sourceCode.text.substring(commentRange[1]));
222
+ const missingNewlines = Math.max(0, numNewlines - emptyLines);
223
+ const eols = eol.repeat(missingNewlines);
200
224
  return fixer.replaceTextRange(
201
- genCommentsRange(context, leadingComments, eol),
202
- genCommentBody(commentType, headerLines, eol, numNewlines)
225
+ commentRange,
226
+ genCommentBody(commentType, headerLines, eol, numNewlines) + eols
227
+ );
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Factory for fixer that replaces an incorrect header.
233
+ * @param {Comment[]} leadingComments comment elements to replace.
234
+ * @param {'\n' | '\r\n'} eol end-of-line characters
235
+ * @param {number} missingEmptyLinesCount number of trailing lines after the
236
+ * comment.
237
+ * @returns {
238
+ * (fixer: RuleTextEditor) => RuleTextEdit | RuleTextEdit[] | null
239
+ * } the fixer.
240
+ */
241
+ function genEmptyLinesFixer(leadingComments, eol, missingEmptyLinesCount) {
242
+ return function(fixer) {
243
+ return fixer.insertTextAfterRange(
244
+ genCommentsRange(leadingComments),
245
+ eol.repeat(missingEmptyLinesCount)
203
246
  );
204
247
  };
205
248
  }
@@ -207,10 +250,11 @@ function genReplaceFixer(commentType, context, leadingComments, headerLines, eol
207
250
  /**
208
251
  * Finds the option parameter within the list of rule config options.
209
252
  * @param {HeaderOptions} options the config options passed to the rule.
210
- * @returns {HeaderSettings | null} the settings parameter or `null` if no such is defined.
253
+ * @returns {HeaderSettings | null} the settings parameter or `null` if no such
254
+ * is defined.
211
255
  */
212
256
  function findSettings(options) {
213
- var lastOption = options.length > 0 ? options[options.length - 1] : null;
257
+ const lastOption = options[options.length - 1];
214
258
  if (typeof lastOption === "object" && !Array.isArray(lastOption) && lastOption !== null
215
259
  && !Object.prototype.hasOwnProperty.call(lastOption, "pattern")) {
216
260
  return lastOption;
@@ -219,12 +263,14 @@ function findSettings(options) {
219
263
  }
220
264
 
221
265
  /**
222
- * Returns the used line-termination characters per the rule's config if any or else based on the runtime environments.
266
+ * Returns the used line-termination characters per the rule's config if any or
267
+ * else based on the runtime environments.
223
268
  * @param {HeaderOptions} options rule configuration.
224
- * @returns {'\n' | '\r\n'} the correct line ending characters for the environment.
269
+ * @returns {'\n' | '\r\n'} the correct line ending characters for the
270
+ * environment.
225
271
  */
226
272
  function getEOL(options) {
227
- var settings = findSettings(options);
273
+ const settings = findSettings(options);
228
274
  if (settings) {
229
275
  if (settings.lineEndings === lineEndingOptions.unix) {
230
276
  return "\n";
@@ -237,132 +283,39 @@ function getEOL(options) {
237
283
  }
238
284
 
239
285
  /**
240
- * Tests if the first line in the source code (after a Unix she-bang) is a comment. Does not tolerate empty lines before the first match.
286
+ * Tests if the first line in the source code (after a Unix she-bang) is a
287
+ * comment. Does not tolerate empty lines before the first match.
241
288
  * @param {string} src source code to test.
242
289
  * @returns {boolean} `true` if there is a comment or `false` otherwise.
243
290
  */
244
- // TODO: check if it is valid to have the copyright notice separated by an empty line from the shebang.
291
+ // TODO: check if it is valid to have the copyright notice separated by an empty
292
+ // line from the shebang.
245
293
  function hasHeader(src) {
246
- if (src.substr(0, 2) === "#!") {
247
- var m = src.match(/(\r\n|\r|\n)/);
294
+ if (src.startsWith("#!")) {
295
+ const m = src.match(/(\r\n|\r|\n)/);
248
296
  if (m) {
249
297
  src = src.slice(m.index + m[0].length);
250
298
  }
251
299
  }
252
- return src.substr(0, 2) === "/*" || src.substr(0, 2) === "//";
253
- }
254
-
255
- /**
256
- * Ensures that the right amount of empty lines trail the header.
257
- * @param {string} src source to validate.
258
- * @param {number} num expected number of trailing empty lines.
259
- * @returns {boolean} `true` if the `num` number of empty lines are appended at the end or `false` otherwise.
260
- */
261
- function matchesLineEndings(src, num) {
262
- for (var j = 0; j < num; ++j) {
263
- var m = src.match(/^(\r\n|\r|\n)/);
264
- if (m) {
265
- src = src.slice(m.index + m[0].length);
266
- } else {
267
- return false;
268
- }
269
- }
270
- return true;
300
+ return src.startsWith("/*") || src.startsWith("//");
271
301
  }
272
302
 
273
303
  module.exports = {
274
304
  meta: {
275
305
  type: "layout",
306
+ docs: {
307
+ description,
308
+ recommended,
309
+ url
310
+ },
276
311
  fixable: "whitespace",
277
- schema: {
278
- $ref: "#/definitions/options",
279
- definitions: {
280
- commentType: {
281
- type: "string",
282
- enum: [commentTypeOptions.block, commentTypeOptions.line]
283
- },
284
- line: {
285
- anyOf: [
286
- {
287
- type: "string"
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
- }
312
+ schema,
313
+ defaultOptions: [{}],
314
+ messages: {
315
+ incorrectCommentType: "header should be a {{commentType}} comment",
316
+ incorrectHeader: "incorrect header",
317
+ missingHeader: "missing header",
318
+ noNewlineAfterHeader: "no newline after header"
366
319
  }
367
320
  },
368
321
  /**
@@ -371,26 +324,27 @@ module.exports = {
371
324
  * @returns {NodeListener} the rule definition.
372
325
  */
373
326
  create: function(context) {
374
- var options = context.options;
375
- var numNewlines = options.length > 2 && typeof options[2] === "number" ? options[2] : 1;
376
- var eol = getEOL(options);
327
+ let options = context.options;
328
+ const numNewlines = options.length > 2 && typeof options[2] === "number" ? options[2] : 1;
329
+ const eol = getEOL(options);
377
330
 
378
331
  // If just one option then read comment from file
379
332
  if (options.length === 1 || (options.length === 2 && findSettings(options))) {
380
- var text = fs.readFileSync(context.options[0], "utf8");
333
+ const text = fs.readFileSync(context.options[0], "utf8");
381
334
  options = commentParser(text);
382
335
  }
383
336
 
384
- var commentType = options[0].toLowerCase();
385
- var headerLines, fixLines = [];
337
+ const commentType = options[0].toLowerCase();
338
+ let headerLines;
339
+ let fixLines = [];
386
340
  // If any of the lines are regular expressions, then we can't
387
341
  // automatically fix them. We set this to true below once we
388
342
  // ensure none of the lines are of type RegExp
389
- var canFix = false;
343
+ let canFix = false;
390
344
  if (Array.isArray(options[1])) {
391
345
  canFix = true;
392
346
  headerLines = options[1].map(function(line) {
393
- var isRegex = isPattern(line);
347
+ const isRegex = isPattern(line);
394
348
  // Can only fix regex option if a template is also provided
395
349
  if (isRegex && !line.template) {
396
350
  canFix = false;
@@ -399,7 +353,7 @@ module.exports = {
399
353
  return isRegex ? new RegExp(line.pattern) : line;
400
354
  });
401
355
  } else if (isPattern(options[1])) {
402
- var line = options[1];
356
+ const line = options[1];
403
357
  headerLines = [new RegExp(line.pattern)];
404
358
  fixLines.push(line.template || line);
405
359
  // Same as above for regex and template
@@ -411,114 +365,138 @@ module.exports = {
411
365
  }
412
366
 
413
367
  return {
368
+ /**
369
+ * Hooks into the processing of the overall script node to do the
370
+ * header validation.
371
+ * @param {Program} node the whole script node
372
+ * @returns {void}
373
+ */
414
374
  Program: function(node) {
415
- if (!hasHeader(context.sourceCode.getText())) {
375
+ if (!hasHeader(context.sourceCode.text)) {
416
376
  context.report({
417
377
  loc: node.loc,
418
- message: "missing header",
378
+ messageId: "missingHeader",
419
379
  fix: genPrependFixer(commentType, context, fixLines, eol, numNewlines)
420
380
  });
421
- } else {
422
- var leadingComments = getLeadingComments(context, node);
381
+ return;
382
+ }
383
+ const leadingComments = getLeadingComments(context, node);
423
384
 
424
- if (!leadingComments.length) {
425
- context.report({
426
- loc: node.loc,
427
- message: "missing header",
428
- fix: canFix ? genPrependFixer(commentType, node, fixLines, eol, numNewlines) : null
429
- });
430
- } else if (leadingComments[0].type.toLowerCase() !== commentType) {
385
+ if (leadingComments[0].type.toLowerCase() !== commentType) {
386
+ context.report({
387
+ loc: node.loc,
388
+ messageId: "incorrectCommentType",
389
+ data: {
390
+ commentType: commentType
391
+ },
392
+ fix: canFix
393
+ ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
394
+ : null
395
+ });
396
+ return;
397
+ }
398
+ if (commentType === commentTypeOptions.line) {
399
+ if (leadingComments.length < headerLines.length) {
431
400
  context.report({
432
401
  loc: node.loc,
433
- message: "header should be a {{commentType}} comment",
434
- data: {
435
- commentType: commentType
436
- },
437
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
402
+ messageId: "incorrectHeader",
403
+ fix: canFix
404
+ ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
405
+ : null
438
406
  });
407
+ return;
408
+ }
409
+ if (headerLines.length === 1) {
410
+ const leadingCommentValues = leadingComments.map((c) => c.value);
411
+ if (
412
+ !match(leadingCommentValues.join("\n"), headerLines[0])
413
+ && !match(leadingCommentValues.join("\r\n"), headerLines[0])
414
+ ) {
415
+ context.report({
416
+ loc: node.loc,
417
+ messageId: "incorrectHeader",
418
+ fix: canFix
419
+ ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
420
+ : null
421
+ });
422
+ return;
423
+ }
439
424
  } else {
440
- if (commentType === commentTypeOptions.line) {
441
- if (leadingComments.length < headerLines.length) {
425
+ for (let i = 0; i < headerLines.length; i++) {
426
+ if (!match(leadingComments[i].value, headerLines[i])) {
442
427
  context.report({
443
428
  loc: node.loc,
444
- message: "incorrect header",
445
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
429
+ messageId: "incorrectHeader",
430
+ fix: canFix
431
+ ? genReplaceFixer(
432
+ commentType,
433
+ context,
434
+ leadingComments,
435
+ fixLines,
436
+ eol,
437
+ numNewlines)
438
+ : null
446
439
  });
447
440
  return;
448
441
  }
449
- if (headerLines.length === 1) {
450
- const leadingCommentValues = leadingComments.map((c) => c.value);
451
- if (!match(leadingCommentValues.join("\n"), headerLines[0]) && !match(leadingCommentValues.join("\r\n"), headerLines[0])) {
452
- context.report({
453
- loc: node.loc,
454
- message: "incorrect header",
455
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
456
- });
457
- return;
458
- }
459
- } else {
460
- for (var i = 0; i < headerLines.length; i++) {
461
- if (!match(leadingComments[i].value, headerLines[i])) {
462
- context.report({
463
- loc: node.loc,
464
- message: "incorrect header",
465
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
466
- });
467
- return;
468
- }
469
- }
470
- }
471
-
472
- var postLineHeader = context.getSourceCode().text.substr(leadingComments[headerLines.length - 1].range[1], numNewlines * 2);
473
- if (!matchesLineEndings(postLineHeader, numNewlines)) {
474
- context.report({
475
- loc: node.loc,
476
- message: "no newline after header",
477
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
478
- });
479
- }
442
+ }
443
+ }
480
444
 
481
- } else {
482
- // if block comment pattern has more than 1 line, we also split the comment
483
- var leadingLines = [leadingComments[0].value];
484
- if (headerLines.length > 1) {
485
- leadingLines = leadingComments[0].value.split(/\r?\n/);
486
- }
445
+ const actualLeadingEmptyLines = leadingEmptyLines(
446
+ context.sourceCode.text.substring(leadingComments[headerLines.length - 1].range[1]));
447
+ const missingEmptyLines = numNewlines - actualLeadingEmptyLines;
448
+ if (missingEmptyLines > 0) {
449
+ context.report({
450
+ loc: node.loc,
451
+ messageId: "noNewlineAfterHeader",
452
+ fix: canFix ? genEmptyLinesFixer(leadingComments, eol, missingEmptyLines) : null
453
+ });
454
+ }
455
+ return;
456
+ }
457
+ // if block comment pattern has more than 1 line, we also split
458
+ // the comment
459
+ let leadingLines = [leadingComments[0].value];
460
+ if (headerLines.length > 1) {
461
+ leadingLines = leadingComments[0].value.split(/\r?\n/);
462
+ }
487
463
 
488
- var hasError = false;
489
- if (leadingLines.length > headerLines.length) {
490
- hasError = true;
491
- }
492
- for (i = 0; !hasError && i < headerLines.length; i++) {
493
- const leadingLine = leadingLines[i];
494
- const headerLine = headerLines[i];
495
- if (!match(leadingLine, headerLine)) {
496
- hasError = true;
497
- break;
498
- }
499
- }
464
+ let hasError = false;
465
+ if (leadingLines.length > headerLines.length) {
466
+ hasError = true;
467
+ }
468
+ for (let i = 0; !hasError && i < headerLines.length; i++) {
469
+ const leadingLine = leadingLines[i];
470
+ const headerLine = headerLines[i];
471
+ if (!match(leadingLine, headerLine)) {
472
+ hasError = true;
473
+ break;
474
+ }
475
+ }
500
476
 
501
- if (hasError) {
502
- if (canFix && headerLines.length > 1) {
503
- fixLines = [fixLines.join(eol)];
504
- }
505
- context.report({
506
- loc: node.loc,
507
- message: "incorrect header",
508
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
509
- });
510
- } else {
511
- var postBlockHeader = context.getSourceCode().text.substr(leadingComments[0].range[1], numNewlines * 2);
512
- if (!matchesLineEndings(postBlockHeader, numNewlines)) {
513
- context.report({
514
- loc: node.loc,
515
- message: "no newline after header",
516
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
517
- });
518
- }
519
- }
520
- }
477
+ if (hasError) {
478
+ if (canFix && headerLines.length > 1) {
479
+ fixLines = [fixLines.join(eol)];
521
480
  }
481
+ context.report({
482
+ loc: node.loc,
483
+ messageId: "incorrectHeader",
484
+ fix: canFix
485
+ ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
486
+ : null
487
+ });
488
+ return;
489
+ }
490
+
491
+ const actualLeadingEmptyLines = leadingEmptyLines(
492
+ context.sourceCode.text.substring(leadingComments[0].range[1]));
493
+ const missingEmptyLines = numNewlines - actualLeadingEmptyLines;
494
+ if (missingEmptyLines > 0) {
495
+ context.report({
496
+ loc: node.loc,
497
+ messageId: "noNewlineAfterHeader",
498
+ fix: canFix ? genEmptyLinesFixer(leadingComments, eol, missingEmptyLines) : null
499
+ });
522
500
  }
523
501
  }
524
502
  };
@@ -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.9",
3
+ "version": "3.1.10",
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,15 +8,21 @@
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=98 --branches=90 --lines=98 --functions=100 mocha tests/lib/**/*.js",
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
19
  "@eslint/js": "^9.32.0",
20
+ "@eslint/markdown": "^7.5.1",
21
+ "@stylistic/eslint-plugin": "^5.5.0",
18
22
  "eslint": "^9.32.0",
23
+ "eslint-plugin-eslint-plugin": "^7.2.0",
19
24
  "eslint-plugin-jsdoc": "^52.0.4",
25
+ "eslint-plugin-n": "^17.23.1",
20
26
  "mocha": "^11.7.1",
21
27
  "nyc": "^17.1.0",
22
28
  "testdouble": "^3.20.2"