@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 CHANGED
@@ -1,4 +1,6 @@
1
- Copyright (c) 2015-present Stuart Knightley, Tony Ganchev and contributors
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 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,
@@ -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 {([string] | [string, HeaderSettings] | [('block' | 'line') | HeaderLines ] | [('block' | 'line') | HeaderLines | HeaderSettings] | [('block' | 'line') | HeaderLines | number ] | [('block' | 'line') | HeaderLines | number | HeaderSettings])} HeaderOptions
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 definition.
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-dfining object or `false` otherwise.
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 against a regex or for full match against a string.
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 config or `false` otherwise.
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 comments from `comments` with the shebang comments omitted.
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
- var all = excludeShebangs(context.getSourceCode().getAllComments(node.body.length ? node.body[0] : node));
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
- for (var i = 1; i < all.length; ++i) {
117
- var txt = context.getSourceCode().getText().slice(all[i - 1].range[1], all[i].range[0]);
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 lines.
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, numNewlines) {
134
- var eols = eol.repeat(numNewlines);
135
+ function genCommentBody(commentType, textArray, eol) {
135
136
  if (commentType === commentTypeOptions.block) {
136
- return "/*" + textArray.join(eol) + "*/" + eols;
137
+ return "/*" + textArray.join(eol) + "*/";
137
138
  } else {
138
- return "//" + textArray.join(eol + "//") + eols;
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
- * @param {RuleContext} context ESLint rule execution context.
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(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
- }
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
- const newHeader = genCommentBody(commentType, headerLines, eol, numNewlines);
172
- if (context.sourceCode.text.substring(0, 2) === "#!") {
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
- 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
- );
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 rule execution context.
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 {(fixer: RuleTextEditor) => RuleTextEdit | RuleTextEdit[] | null} the fixer.
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
- genCommentsRange(context, leadingComments, eol),
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 is defined.
254
+ * @returns {HeaderSettings | null} the settings parameter or `null` if no such
255
+ * is defined.
211
256
  */
212
257
  function findSettings(options) {
213
- var lastOption = options.length > 0 ? options[options.length - 1] : null;
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 else based on the runtime environments.
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 environment.
270
+ * @returns {'\n' | '\r\n'} the correct line ending characters for the
271
+ * environment.
225
272
  */
226
273
  function getEOL(options) {
227
- var settings = findSettings(options);
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 comment. Does not tolerate empty lines before the first match.
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
- if (src.substr(0, 2) === "#!") {
247
- var m = src.match(/(\r\n|\r|\n)/);
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
- * 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.
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 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;
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
- $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
- }
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
- var options = context.options;
375
- var numNewlines = options.length > 2 && typeof options[2] === "number" ? options[2] : 1;
376
- var eol = getEOL(options);
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
- var text = fs.readFileSync(context.options[0], "utf8");
357
+ const text = fs.readFileSync(context.options[0], "utf8");
381
358
  options = commentParser(text);
382
359
  }
383
360
 
384
- var commentType = options[0].toLowerCase();
385
- var headerLines, fixLines = [];
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
- var canFix = false;
368
+ let canFix = false;
390
369
  if (Array.isArray(options[1])) {
391
370
  canFix = true;
392
371
  headerLines = options[1].map(function(line) {
393
- var isRegex = isPattern(line);
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
- var line = options[1];
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.getText())) {
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: node.loc,
418
- message: "missing header",
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
- } else {
422
- var leadingComments = getLeadingComments(context, node);
417
+ return;
418
+ }
419
+ const leadingComments = getLeadingComments(context, node);
423
420
 
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) {
431
- context.report({
432
- 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
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
- if (commentType === commentTypeOptions.line) {
441
- if (leadingComments.length < headerLines.length) {
457
+ for (let i = 0; i < headerLines.length; i++) {
458
+ if (leadingComments.length - 1 < i) {
442
459
  context.report({
443
- loc: node.loc,
444
- message: "incorrect header",
445
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
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.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])) {
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: node.loc,
454
- message: "incorrect header",
455
- fix: canFix ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines) : null
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
- 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
- }
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
- 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
- }
480
-
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
- }
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
- var hasError = false;
489
- if (leadingLines.length > headerLines.length) {
490
- hasError = true;
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
- 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;
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
- if (hasError) {
502
- if (canFix && headerLines.length > 1) {
503
- fixLines = [fixLines.join(eol)];
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
- 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
- });
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.9",
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=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
- "@eslint/js": "^9.32.0",
18
- "eslint": "^9.32.0",
19
- "eslint-plugin-jsdoc": "^52.0.4",
20
- "mocha": "^11.7.1",
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
  },