@tony.ganchev/eslint-plugin-header 3.3.4 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -8
- package/lib/comment-parser.js +23 -9
- package/lib/rules/header.js +366 -220
- package/package.json +18 -8
- package/types/lib/comment-parser.d.ts +3 -1
- package/types/lib/comment-parser.d.ts.map +1 -1
- package/types/lib/rules/header.d.ts +62 -1
- package/types/lib/rules/header.d.ts.map +1 -1
package/lib/rules/header.js
CHANGED
|
@@ -178,60 +178,156 @@ function match(actual, expected) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* @typedef {[number, number]} Range
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @typedef {object} CommentEx
|
|
187
|
+
* @property {"Shebang" | "Block" | "Line"} type Comment type.
|
|
188
|
+
* @property {string} value Content of the comment.
|
|
189
|
+
* @property {Range} range Linear position of the comment within the source.
|
|
190
|
+
* @property {SourceLocation} loc Editor-oriented location of the comment.
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @typedef {object} BlockComment
|
|
195
|
+
* @property {string} startDelimiter The delimiter that marks the start of the
|
|
196
|
+
* block comment.
|
|
197
|
+
* @property {string} endDelimiter The delimiter that marks the end of the
|
|
198
|
+
* block comment.
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @typedef {object} LineComment
|
|
203
|
+
* @property {string} startDelimiter The delimiter that marks the start of the
|
|
204
|
+
* line comment.
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @typedef {object} Language
|
|
209
|
+
* @property { BlockComment } blockComment The tokens delimiting a block
|
|
210
|
+
* comment.
|
|
211
|
+
* @property { LineComment } [lineComment] The tokens delimiting a line comment
|
|
212
|
+
* if such is allowed.
|
|
213
|
+
* @property { boolean} [shebang] If set to `true`, the logic to handle shebangs
|
|
214
|
+
* will kick in.
|
|
215
|
+
*/
|
|
216
|
+
|
|
181
217
|
/**
|
|
182
218
|
* Returns an array of comment groups before the actual code.
|
|
183
219
|
* Block comments form single-element groups.
|
|
184
220
|
* Line comments without empty lines between them form grouped elements.
|
|
185
221
|
* @param {SourceCode} sourceCode AST.
|
|
186
|
-
* @
|
|
222
|
+
* @param {Language} language The language configuration.
|
|
223
|
+
* @returns {CommentEx[][]} Array of groups of leading comments.
|
|
187
224
|
*/
|
|
188
|
-
function getLeadingComments(sourceCode) {
|
|
189
|
-
const
|
|
190
|
-
if (all.length === 0) {
|
|
191
|
-
return [];
|
|
192
|
-
}
|
|
193
|
-
// Determine where the actual code starts. If no code, use end of file.
|
|
194
|
-
const firstToken = sourceCode.getFirstToken(sourceCode.ast);
|
|
195
|
-
const codeStart = firstToken ? firstToken.range[0] : sourceCode.text.length;
|
|
196
|
-
// Filter comments that appear before the actual code starts.
|
|
197
|
-
const commentsBeforeCode = all.filter((c) => /** @type {[number, number]} */(c.range)[1] <= codeStart);
|
|
198
|
-
if (commentsBeforeCode.length === 0) {
|
|
199
|
-
return [];
|
|
200
|
-
}
|
|
201
|
-
/** @type {Comment[][]} */
|
|
225
|
+
function getLeadingComments(sourceCode, language) {
|
|
226
|
+
const text = sourceCode.text;
|
|
202
227
|
const groups = [];
|
|
203
|
-
/** @type {
|
|
228
|
+
/** @type {CommentEx[]} */
|
|
204
229
|
let currentGroup = [];
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
let pos = 0;
|
|
231
|
+
let lastCommentEnd = 0;
|
|
207
232
|
|
|
208
|
-
|
|
209
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Adds a comment to the appropriate group, handling line-comment grouping.
|
|
235
|
+
* @param {CommentEx} comment The comment to add.
|
|
236
|
+
* @returns {void}
|
|
237
|
+
*/
|
|
238
|
+
function addComment(comment) {
|
|
239
|
+
if (comment.type === "Block" || comment.type === "Shebang") {
|
|
210
240
|
if (currentGroup.length > 0) {
|
|
211
241
|
groups.push(currentGroup);
|
|
212
242
|
currentGroup = [];
|
|
213
243
|
}
|
|
214
244
|
groups.push([comment]);
|
|
215
245
|
} else {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const txt = sourceCode.text.slice(previousRange[1], currentRange[0]);
|
|
223
|
-
|
|
224
|
-
// If there is more than 1 newline, there is an empty line
|
|
225
|
-
// between comments.
|
|
226
|
-
const newlineCount = /** @type {RegExpMatchArray} */ (txt.match(/\r?\n/g)).length;
|
|
227
|
-
if (newlineCount <= 1) {
|
|
228
|
-
currentGroup.push(comment);
|
|
229
|
-
} else {
|
|
246
|
+
// Group consecutive line comments
|
|
247
|
+
if (currentGroup.length > 0) {
|
|
248
|
+
const range = comment.range;
|
|
249
|
+
const whitespace = text.slice(lastCommentEnd, range[0]);
|
|
250
|
+
const newlineCount = whitespace.split(/\r?\n/).length - 1;
|
|
251
|
+
if (newlineCount > 1) {
|
|
230
252
|
groups.push(currentGroup);
|
|
231
|
-
currentGroup = [
|
|
253
|
+
currentGroup = [];
|
|
232
254
|
}
|
|
233
255
|
}
|
|
256
|
+
currentGroup.push(comment);
|
|
234
257
|
}
|
|
258
|
+
lastCommentEnd = comment.range[1];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
while (pos < text.length) {
|
|
262
|
+
// Try Shebang only at the very start
|
|
263
|
+
if (language.shebang && pos === 0) {
|
|
264
|
+
const shebangMatch = text.match(/^#!.*/);
|
|
265
|
+
if (shebangMatch) {
|
|
266
|
+
const shebangText = shebangMatch[0];
|
|
267
|
+
const valueText = shebangText.slice(2);
|
|
268
|
+
addComment({
|
|
269
|
+
type: "Shebang",
|
|
270
|
+
value: valueText,
|
|
271
|
+
range: [0, shebangText.length],
|
|
272
|
+
loc: {
|
|
273
|
+
start: sourceCode.getLocFromIndex(0),
|
|
274
|
+
end: sourceCode.getLocFromIndex(shebangText.length)
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
pos = shebangText.length;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check for block comment
|
|
283
|
+
if (language.blockComment && text.startsWith(language.blockComment.startDelimiter, pos)) {
|
|
284
|
+
const endIdx =
|
|
285
|
+
text.indexOf(language.blockComment.endDelimiter, pos + language.blockComment.startDelimiter.length);
|
|
286
|
+
if (endIdx !== -1) {
|
|
287
|
+
const valueText = text.slice(pos + language.blockComment.startDelimiter.length, endIdx);
|
|
288
|
+
const endPos = endIdx + language.blockComment.endDelimiter.length;
|
|
289
|
+
addComment({
|
|
290
|
+
type: "Block",
|
|
291
|
+
value: valueText,
|
|
292
|
+
range: [pos, endPos],
|
|
293
|
+
loc: {
|
|
294
|
+
start: sourceCode.getLocFromIndex(pos),
|
|
295
|
+
end: sourceCode.getLocFromIndex(endPos)
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
pos = endPos;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check for line comment
|
|
304
|
+
if (language.lineComment && text.startsWith(language.lineComment.startDelimiter, pos)) {
|
|
305
|
+
const endOfLineMatch = text.slice(pos).match(/\r?\n/);
|
|
306
|
+
const endOffset = endOfLineMatch ? (/** @type {number} */ (endOfLineMatch.index)) : text.length - pos;
|
|
307
|
+
const endIdx = pos + endOffset;
|
|
308
|
+
const valueText = text.slice(pos + language.lineComment.startDelimiter.length, endIdx);
|
|
309
|
+
addComment({
|
|
310
|
+
type: "Line",
|
|
311
|
+
value: valueText,
|
|
312
|
+
range: [pos, endIdx],
|
|
313
|
+
loc: {
|
|
314
|
+
start: sourceCode.getLocFromIndex(pos),
|
|
315
|
+
end: sourceCode.getLocFromIndex(endIdx)
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
pos = endIdx;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Check for whitespace to skip
|
|
323
|
+
const wsMatch = text.slice(pos).match(/^\s+/);
|
|
324
|
+
if (wsMatch) {
|
|
325
|
+
pos += wsMatch[0].length;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Anything else is code, stop parsing leading comments
|
|
330
|
+
break;
|
|
235
331
|
}
|
|
236
332
|
|
|
237
333
|
if (currentGroup.length > 0) {
|
|
@@ -247,32 +343,32 @@ function getLeadingComments(sourceCode) {
|
|
|
247
343
|
* @param {CommentType} commentType The type of comment to generate.
|
|
248
344
|
* @param {string[]} textArray List of lines of the comment content.
|
|
249
345
|
* @param {LineEnding} eol End-of-line characters.
|
|
346
|
+
* @param {Language} language The language configuration.
|
|
250
347
|
* @returns {string} Resulting comment.
|
|
251
348
|
*/
|
|
252
|
-
function genCommentBody(commentType, textArray, eol) {
|
|
349
|
+
function genCommentBody(commentType, textArray, eol, language) {
|
|
253
350
|
if (commentType === commentTypeOptions.block) {
|
|
254
|
-
return
|
|
351
|
+
return language.blockComment.startDelimiter + textArray.join(eol) + language.blockComment.endDelimiter;
|
|
255
352
|
} else {
|
|
353
|
+
const lineStartDelimiter = /** @type {LineComment} */ (language.lineComment).startDelimiter;
|
|
256
354
|
// We need one trailing EOL on line comments to ensure the fixed source
|
|
257
355
|
// is parsable.
|
|
258
|
-
return
|
|
356
|
+
return lineStartDelimiter + textArray.join(eol + lineStartDelimiter);
|
|
259
357
|
}
|
|
260
358
|
}
|
|
261
359
|
|
|
262
360
|
/**
|
|
263
361
|
* Determines the start and end position in the source code of the leading
|
|
264
362
|
* comment.
|
|
265
|
-
* @param {
|
|
266
|
-
* @returns {
|
|
363
|
+
* @param {CommentEx[]} comments List of comments.
|
|
364
|
+
* @returns {Range} Resulting range.
|
|
267
365
|
*/
|
|
268
366
|
function genCommentsRange(comments) {
|
|
269
367
|
assert.ok(comments.length);
|
|
270
368
|
const firstComment = comments[0];
|
|
271
|
-
const
|
|
272
|
-
const start = firstCommentRange[0];
|
|
369
|
+
const start = firstComment.range[0];
|
|
273
370
|
const lastComment = comments.slice(-1)[0];
|
|
274
|
-
const
|
|
275
|
-
const end = lastCommentRange[1];
|
|
371
|
+
const end = lastComment.range[1];
|
|
276
372
|
return [start, end];
|
|
277
373
|
}
|
|
278
374
|
|
|
@@ -303,13 +399,14 @@ function leadingEmptyLines(src) {
|
|
|
303
399
|
* @param {string[]} headerLines Lines of the header comment.
|
|
304
400
|
* @param {LineEnding} eol End-of-line characters.
|
|
305
401
|
* @param {number} numNewlines Number of trailing lines after the comment.
|
|
402
|
+
* @param {Language} language The language configuration.
|
|
306
403
|
* @returns {ReportFixer} The fix to apply.
|
|
307
404
|
*/
|
|
308
|
-
function genPrependFixer(commentType, sourceCode, headerLines, eol, numNewlines) {
|
|
405
|
+
function genPrependFixer(commentType, sourceCode, headerLines, eol, numNewlines, language) {
|
|
309
406
|
return function (fixer) {
|
|
310
407
|
let insertPos = 0;
|
|
311
|
-
let newHeader = genCommentBody(commentType, headerLines, eol);
|
|
312
|
-
if (sourceCode.text.startsWith("#!")) {
|
|
408
|
+
let newHeader = genCommentBody(commentType, headerLines, eol, language);
|
|
409
|
+
if (language.shebang && sourceCode.text.startsWith("#!")) {
|
|
313
410
|
const firstNewLinePos = sourceCode.text.indexOf("\n");
|
|
314
411
|
insertPos = firstNewLinePos === -1 ? sourceCode.text.length : firstNewLinePos + 1;
|
|
315
412
|
if (firstNewLinePos === -1) {
|
|
@@ -330,13 +427,14 @@ function genPrependFixer(commentType, sourceCode, headerLines, eol, numNewlines)
|
|
|
330
427
|
* Factory for fixer that replaces an incorrect header.
|
|
331
428
|
* @param {CommentType} commentType Type of comment to use.
|
|
332
429
|
* @param {SourceCode} sourceCode AST.
|
|
333
|
-
* @param {
|
|
430
|
+
* @param {CommentEx[]} leadingComments Comment elements to replace.
|
|
334
431
|
* @param {string[]} headerLines Lines of the header comment.
|
|
335
432
|
* @param {LineEnding} eol End-of-line characters.
|
|
336
433
|
* @param {number} numNewlines Number of trailing lines after the comment.
|
|
434
|
+
* @param {Language} language The language configuration.
|
|
337
435
|
* @returns {ReportFixer} The fix to apply.
|
|
338
436
|
*/
|
|
339
|
-
function genReplaceFixer(commentType, sourceCode, leadingComments, headerLines, eol, numNewlines) {
|
|
437
|
+
function genReplaceFixer(commentType, sourceCode, leadingComments, headerLines, eol, numNewlines, language) {
|
|
340
438
|
return function (fixer) {
|
|
341
439
|
const commentRange = genCommentsRange(leadingComments);
|
|
342
440
|
const emptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
|
|
@@ -344,14 +442,14 @@ function genReplaceFixer(commentType, sourceCode, leadingComments, headerLines,
|
|
|
344
442
|
const eols = eol.repeat(missingNewlines);
|
|
345
443
|
return fixer.replaceTextRange(
|
|
346
444
|
commentRange,
|
|
347
|
-
genCommentBody(commentType, headerLines, eol) + eols
|
|
445
|
+
genCommentBody(commentType, headerLines, eol, language) + eols
|
|
348
446
|
);
|
|
349
447
|
};
|
|
350
448
|
}
|
|
351
449
|
|
|
352
450
|
/**
|
|
353
451
|
* Factory for fixer that replaces an incorrect header.
|
|
354
|
-
* @param {
|
|
452
|
+
* @param {CommentEx[]} leadingComments Comment elements to replace.
|
|
355
453
|
* @param {LineEnding} eol End-of-line characters.
|
|
356
454
|
* @param {number} missingEmptyLinesCount Number of trailing lines after the
|
|
357
455
|
* comment.
|
|
@@ -481,17 +579,24 @@ function isFileBasedHeaderConfig(config) {
|
|
|
481
579
|
return "file" in config;
|
|
482
580
|
}
|
|
483
581
|
|
|
582
|
+
/** @type {Record<string, { commentType: CommentType; lines: string[]}>} */
|
|
583
|
+
const parsedFilesCache = {};
|
|
584
|
+
|
|
484
585
|
/**
|
|
485
586
|
* Transforms file template-based matching rules to inline rules for further
|
|
486
587
|
* use.
|
|
487
588
|
* @param {FileBasedConfig | InlineConfig} matcher The matching rule.
|
|
589
|
+
* @param {Language} language The language configuration.
|
|
488
590
|
* @returns {InlineConfig} The resulting normalized configuration.
|
|
489
591
|
*/
|
|
490
|
-
function normalizeMatchingRules(matcher) {
|
|
592
|
+
function normalizeMatchingRules(matcher, language) {
|
|
491
593
|
if (isFileBasedHeaderConfig(matcher)) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
594
|
+
if (!(matcher.file in parsedFilesCache)) {
|
|
595
|
+
const text = fs.readFileSync(matcher.file, matcher.encoding || "utf8");
|
|
596
|
+
const [commentType, lines] = commentParser(text, language);
|
|
597
|
+
parsedFilesCache[matcher.file] = { commentType, lines };
|
|
598
|
+
}
|
|
599
|
+
return parsedFilesCache[matcher.file];
|
|
495
600
|
}
|
|
496
601
|
const commentType = matcher.commentType;
|
|
497
602
|
const lines = matcher.lines.flatMap(
|
|
@@ -519,12 +624,13 @@ function normalizeMatchingRules(matcher) {
|
|
|
519
624
|
* Transforms a set of new-style options adding defaults and standardizing on
|
|
520
625
|
* one of multiple config styles.
|
|
521
626
|
* @param {HeaderOptions} originalOptions New-style options to normalize.
|
|
627
|
+
* @param {Language} language The language configuration.
|
|
522
628
|
* @returns {HeaderOptions} Normalized options.
|
|
523
629
|
*/
|
|
524
|
-
function normalizeOptions(originalOptions) {
|
|
630
|
+
function normalizeOptions(originalOptions, language) {
|
|
525
631
|
const options = structuredClone(originalOptions);
|
|
526
632
|
|
|
527
|
-
options.header = normalizeMatchingRules(originalOptions.header);
|
|
633
|
+
options.header = normalizeMatchingRules(originalOptions.header, language);
|
|
528
634
|
|
|
529
635
|
if (!options.lineEndings) {
|
|
530
636
|
options.lineEndings = "os";
|
|
@@ -532,7 +638,7 @@ function normalizeOptions(originalOptions) {
|
|
|
532
638
|
|
|
533
639
|
if (originalOptions.leadingComments) {
|
|
534
640
|
options.leadingComments = {
|
|
535
|
-
comments: originalOptions.leadingComments.comments.map((c) => normalizeMatchingRules(c))
|
|
641
|
+
comments: originalOptions.leadingComments.comments.map((c) => normalizeMatchingRules(c, language))
|
|
536
642
|
};
|
|
537
643
|
} else {
|
|
538
644
|
options.leadingComments = { comments: [] };
|
|
@@ -553,7 +659,7 @@ function normalizeOptions(originalOptions) {
|
|
|
553
659
|
* insufficient) lines that trail the comment. A special case is when there are
|
|
554
660
|
* no empty lines after the header in which case we highlight the next character
|
|
555
661
|
* in the source regardless of which one it is).
|
|
556
|
-
* @param {
|
|
662
|
+
* @param {CommentEx[]} leadingComments The comment lines that constitute the
|
|
557
663
|
* header.
|
|
558
664
|
* @param {number} actualEmptyLines The number of empty lines that follow the
|
|
559
665
|
* header.
|
|
@@ -561,7 +667,7 @@ function normalizeOptions(originalOptions) {
|
|
|
561
667
|
*/
|
|
562
668
|
function missingEmptyLinesViolationLoc(leadingComments, actualEmptyLines) {
|
|
563
669
|
assert.ok(leadingComments);
|
|
564
|
-
const loc =
|
|
670
|
+
const loc = leadingComments[leadingComments.length - 1].loc;
|
|
565
671
|
const lastCommentLineLocEnd = loc.end;
|
|
566
672
|
return {
|
|
567
673
|
start: lastCommentLineLocEnd,
|
|
@@ -584,12 +690,14 @@ class CommentMatcher {
|
|
|
584
690
|
* @param {string} eol The EOL characters used.
|
|
585
691
|
* @param {number} numLines The requirred minimum number of trailing empty
|
|
586
692
|
* lines.
|
|
693
|
+
* @param {Language} language The language configuration.
|
|
587
694
|
*/
|
|
588
|
-
constructor(headerConfig, eol, numLines) {
|
|
695
|
+
constructor(headerConfig, eol, numLines, language) {
|
|
589
696
|
this.commentType = headerConfig.commentType;
|
|
590
697
|
this.headerLines = headerConfig.lines.map((line) => isPattern(line) ? line.pattern : line);
|
|
591
698
|
this.eol = eol;
|
|
592
699
|
this.numLines = numLines;
|
|
700
|
+
this.language = language;
|
|
593
701
|
}
|
|
594
702
|
|
|
595
703
|
/**
|
|
@@ -600,18 +708,18 @@ class CommentMatcher {
|
|
|
600
708
|
/**
|
|
601
709
|
* Performs a validation of a comment against a header matching
|
|
602
710
|
* configuration.
|
|
603
|
-
* @param {
|
|
604
|
-
* comments to test.
|
|
711
|
+
* @param {CommentEx[]} leadingComments The block comment or sequence of
|
|
712
|
+
* line comments to test.
|
|
605
713
|
* @param {SourceCode} sourceCode The source code AST.
|
|
606
714
|
* @returns {ViolationReportEx | null} If set a
|
|
607
715
|
* violation report to pass back to ESLint or interpret as necessary.
|
|
608
716
|
*/
|
|
609
717
|
validate(leadingComments, sourceCode) {
|
|
610
718
|
|
|
611
|
-
const firstLeadingCommentLoc =
|
|
612
|
-
const firstLeadingCommentRange =
|
|
719
|
+
const firstLeadingCommentLoc = leadingComments[0].loc;
|
|
720
|
+
const firstLeadingCommentRange = leadingComments[0].range;
|
|
613
721
|
|
|
614
|
-
const lastLeadingCommentLoc =
|
|
722
|
+
const lastLeadingCommentLoc = leadingComments[leadingComments.length - 1].loc;
|
|
615
723
|
|
|
616
724
|
if (leadingComments[0].type.toLowerCase() !== this.commentType) {
|
|
617
725
|
return {
|
|
@@ -626,6 +734,7 @@ class CommentMatcher {
|
|
|
626
734
|
};
|
|
627
735
|
}
|
|
628
736
|
if (this.commentType === commentTypeOptions.line) {
|
|
737
|
+
const lineCommentDelimiter = /** @type {LineComment} */ (this.language.lineComment).startDelimiter;
|
|
629
738
|
if (this.headerLines.length === 1) {
|
|
630
739
|
const leadingCommentValues = leadingComments.map((c) => c.value);
|
|
631
740
|
if (
|
|
@@ -659,7 +768,7 @@ class CommentMatcher {
|
|
|
659
768
|
}
|
|
660
769
|
const headerLine = this.headerLines[i];
|
|
661
770
|
const comment = leadingComments[i];
|
|
662
|
-
const commentLoc =
|
|
771
|
+
const commentLoc = comment.loc;
|
|
663
772
|
if (typeof headerLine === "string") {
|
|
664
773
|
const leadingCommentLength = comment.value.length;
|
|
665
774
|
const headerLineLength = headerLine.length;
|
|
@@ -668,7 +777,7 @@ class CommentMatcher {
|
|
|
668
777
|
return {
|
|
669
778
|
loc: {
|
|
670
779
|
start: {
|
|
671
|
-
column:
|
|
780
|
+
column: lineCommentDelimiter.length + j,
|
|
672
781
|
line: commentLoc.start.line
|
|
673
782
|
},
|
|
674
783
|
end: commentLoc.end
|
|
@@ -696,7 +805,7 @@ class CommentMatcher {
|
|
|
696
805
|
return {
|
|
697
806
|
loc: {
|
|
698
807
|
start: {
|
|
699
|
-
column:
|
|
808
|
+
column: lineCommentDelimiter.length + headerLineLength,
|
|
700
809
|
line: commentLoc.start.line
|
|
701
810
|
},
|
|
702
811
|
end: commentLoc.end,
|
|
@@ -709,7 +818,7 @@ class CommentMatcher {
|
|
|
709
818
|
return {
|
|
710
819
|
loc: {
|
|
711
820
|
start: {
|
|
712
|
-
column:
|
|
821
|
+
column: lineCommentDelimiter.length,
|
|
713
822
|
line: commentLoc.start.line,
|
|
714
823
|
},
|
|
715
824
|
end: commentLoc.end,
|
|
@@ -724,7 +833,7 @@ class CommentMatcher {
|
|
|
724
833
|
}
|
|
725
834
|
}
|
|
726
835
|
|
|
727
|
-
const commentRange =
|
|
836
|
+
const commentRange = leadingComments[this.headerLines.length - 1].range;
|
|
728
837
|
const actualLeadingEmptyLines = leadingEmptyLines(sourceCode.text.substring(commentRange[1]));
|
|
729
838
|
const missingEmptyLines = this.numLines - actualLeadingEmptyLines;
|
|
730
839
|
if (missingEmptyLines > 0) {
|
|
@@ -753,6 +862,9 @@ class CommentMatcher {
|
|
|
753
862
|
let errorMessageData;
|
|
754
863
|
/** @type {null | SourceLocation} */
|
|
755
864
|
let errorMessageLoc = null;
|
|
865
|
+
const blockStartLen = this.language.blockComment.startDelimiter.length;
|
|
866
|
+
const blockEndLen = this.language.blockComment.endDelimiter.length;
|
|
867
|
+
|
|
756
868
|
for (let i = 0; i < this.headerLines.length; i++) {
|
|
757
869
|
if (leadingLines.length - 1 < i) {
|
|
758
870
|
return {
|
|
@@ -772,54 +884,39 @@ class CommentMatcher {
|
|
|
772
884
|
for (let j = 0; j < Math.min(leadingLine.length, headerLine.length); j++) {
|
|
773
885
|
if (leadingLine[j] !== headerLine[j]) {
|
|
774
886
|
errorMessageId = "headerLineMismatchAtPos";
|
|
775
|
-
const columnOffset = i === 0 ?
|
|
887
|
+
const columnOffset = i === 0 ? blockStartLen : 0;
|
|
776
888
|
const line = firstLeadingCommentLoc.start.line + i;
|
|
777
889
|
errorMessageLoc = {
|
|
778
|
-
start: {
|
|
779
|
-
|
|
780
|
-
line
|
|
781
|
-
},
|
|
782
|
-
end: {
|
|
783
|
-
column: columnOffset + leadingLine.length,
|
|
784
|
-
line
|
|
785
|
-
}
|
|
786
|
-
};
|
|
787
|
-
errorMessageData = {
|
|
788
|
-
expected: headerLine.substring(j)
|
|
890
|
+
start: { column: columnOffset + j, line },
|
|
891
|
+
end: { column: columnOffset + leadingLine.length, line }
|
|
789
892
|
};
|
|
893
|
+
errorMessageData = { expected: headerLine.substring(j) };
|
|
790
894
|
break;
|
|
791
895
|
}
|
|
792
896
|
}
|
|
793
897
|
if (errorMessageId) {
|
|
794
898
|
break;
|
|
795
899
|
}
|
|
900
|
+
|
|
796
901
|
if (leadingLine.length < headerLine.length) {
|
|
797
902
|
errorMessageId = "headerLineTooShort";
|
|
798
|
-
const startColumn = (i === 0 ?
|
|
903
|
+
const startColumn = (i === 0 ? blockStartLen : 0) + leadingLine.length;
|
|
799
904
|
errorMessageLoc = {
|
|
800
|
-
start: {
|
|
801
|
-
|
|
802
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
803
|
-
},
|
|
804
|
-
end: {
|
|
805
|
-
column: startColumn + 1,
|
|
806
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
errorMessageData = {
|
|
810
|
-
remainder: headerLine.substring(leadingLine.length)
|
|
905
|
+
start: { column: startColumn, line: firstLeadingCommentLoc.start.line + i },
|
|
906
|
+
end: { column: startColumn + 1, line: firstLeadingCommentLoc.start.line + i }
|
|
811
907
|
};
|
|
908
|
+
errorMessageData = { remainder: headerLine.substring(leadingLine.length) };
|
|
812
909
|
break;
|
|
813
910
|
}
|
|
814
911
|
if (leadingLine.length > headerLine.length) {
|
|
815
912
|
errorMessageId = "headerLineTooLong";
|
|
816
913
|
errorMessageLoc = {
|
|
817
914
|
start: {
|
|
818
|
-
column: (i === 0 ?
|
|
915
|
+
column: (i === 0 ? blockStartLen : 0) + headerLine.length,
|
|
819
916
|
line: firstLeadingCommentLoc.start.line + i
|
|
820
917
|
},
|
|
821
918
|
end: {
|
|
822
|
-
column: (i === 0 ?
|
|
919
|
+
column: (i === 0 ? blockStartLen : 0) + leadingLine.length,
|
|
823
920
|
line: firstLeadingCommentLoc.start.line + i
|
|
824
921
|
}
|
|
825
922
|
};
|
|
@@ -828,19 +925,11 @@ class CommentMatcher {
|
|
|
828
925
|
} else {
|
|
829
926
|
if (!match(leadingLine, headerLine)) {
|
|
830
927
|
errorMessageId = "incorrectHeaderLine";
|
|
831
|
-
errorMessageData = {
|
|
832
|
-
|
|
833
|
-
};
|
|
834
|
-
const columnOffset = i === 0 ? "/*".length : 0;
|
|
928
|
+
errorMessageData = { pattern: headerLine.toString() };
|
|
929
|
+
const columnOffset = i === 0 ? blockStartLen : 0;
|
|
835
930
|
errorMessageLoc = {
|
|
836
|
-
start: {
|
|
837
|
-
|
|
838
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
839
|
-
},
|
|
840
|
-
end: {
|
|
841
|
-
column: columnOffset + leadingLine.length,
|
|
842
|
-
line: firstLeadingCommentLoc.start.line + i
|
|
843
|
-
}
|
|
931
|
+
start: { column: columnOffset + 0, line: firstLeadingCommentLoc.start.line + i },
|
|
932
|
+
end: { column: columnOffset + leadingLine.length, line: firstLeadingCommentLoc.start.line + i }
|
|
844
933
|
};
|
|
845
934
|
break;
|
|
846
935
|
}
|
|
@@ -851,11 +940,11 @@ class CommentMatcher {
|
|
|
851
940
|
errorMessageId = "headerTooLong";
|
|
852
941
|
errorMessageLoc = {
|
|
853
942
|
start: {
|
|
854
|
-
column: (this.headerLines.length === 0 ?
|
|
943
|
+
column: (this.headerLines.length === 0 ? blockStartLen : 0) + 0,
|
|
855
944
|
line: firstLeadingCommentLoc.start.line + this.headerLines.length
|
|
856
945
|
},
|
|
857
946
|
end: {
|
|
858
|
-
column: lastLeadingCommentLoc.end.column -
|
|
947
|
+
column: lastLeadingCommentLoc.end.column - blockEndLen,
|
|
859
948
|
line: lastLeadingCommentLoc.end.line
|
|
860
949
|
}
|
|
861
950
|
};
|
|
@@ -927,7 +1016,8 @@ const headerRule = {
|
|
|
927
1016
|
"leading comment validation failed: comment line does not match pattern: '{{pattern}}'",
|
|
928
1017
|
// messages only applicable to header validation.
|
|
929
1018
|
missingHeader: "missing header",
|
|
930
|
-
noNewlineAfterHeader: "not enough newlines after header: expected: {{expected}}, actual: {{actual}}"
|
|
1019
|
+
noNewlineAfterHeader: "not enough newlines after header: expected: {{expected}}, actual: {{actual}}",
|
|
1020
|
+
unsupportedLineHeader: "line header configured but not supported for this language"
|
|
931
1021
|
}
|
|
932
1022
|
},
|
|
933
1023
|
|
|
@@ -939,125 +1029,72 @@ const headerRule = {
|
|
|
939
1029
|
create: function (context) {
|
|
940
1030
|
|
|
941
1031
|
const newStyleOptions = transformLegacyOptions(/** @type {AllHeaderOptions} */(context.options));
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
/**
|
|
960
|
-
* Hooks into the processing of the overall script node to do the
|
|
961
|
-
* header validation.
|
|
962
|
-
* @returns {void}
|
|
963
|
-
*/
|
|
964
|
-
Program: function () {
|
|
965
|
-
const sourceCode = contextSourceCode(context);
|
|
966
|
-
const leadingComments = getLeadingComments(sourceCode);
|
|
967
|
-
// XXX: the reason we test like this instead of checking the
|
|
968
|
-
// first comment is of type "Shabeng" is because
|
|
969
|
-
// @typecript-eslint/prser does not recognize shebang comments
|
|
970
|
-
// with some TypeScript configuration. Since we are not
|
|
971
|
-
// releasing a major version we do not want to break the current
|
|
972
|
-
// behavior of not be pedantic about the tsconfig.json.
|
|
973
|
-
const hasShebang = sourceCode.text.startsWith("#!");
|
|
974
|
-
let startingHeaderLine = 1;
|
|
975
|
-
if (hasShebang) {
|
|
976
|
-
if (leadingComments.length > 0
|
|
977
|
-
&& /** @type {string} */ (leadingComments[0][0].type) === "Shebang"
|
|
978
|
-
) {
|
|
979
|
-
leadingComments.splice(0, 1);
|
|
980
|
-
}
|
|
981
|
-
startingHeaderLine = 2;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
if (leadingComments.length === 0
|
|
985
|
-
|| /** @type {SourceLocation} */ (leadingComments[0][0].loc).start.line > startingHeaderLine) {
|
|
986
|
-
|
|
987
|
-
context.report({
|
|
988
|
-
loc: {
|
|
989
|
-
start: {
|
|
990
|
-
column: 0,
|
|
991
|
-
line: startingHeaderLine
|
|
992
|
-
},
|
|
993
|
-
end: {
|
|
994
|
-
column: 0,
|
|
995
|
-
line: startingHeaderLine
|
|
996
|
-
}
|
|
997
|
-
},
|
|
998
|
-
messageId: "missingHeader",
|
|
999
|
-
fix: canFix
|
|
1000
|
-
? genPrependFixer(
|
|
1001
|
-
headerMatcher.commentType,
|
|
1002
|
-
sourceCode,
|
|
1003
|
-
fixLines,
|
|
1004
|
-
eol,
|
|
1005
|
-
numLines)
|
|
1006
|
-
: null
|
|
1007
|
-
});
|
|
1008
|
-
return;
|
|
1032
|
+
const sourceCode = contextSourceCode(context);
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Hooks into the processing of the overall script node to do the
|
|
1036
|
+
* header validation.
|
|
1037
|
+
* @param {Language} language The language configuration.
|
|
1038
|
+
* @returns {void}
|
|
1039
|
+
*/
|
|
1040
|
+
function onRootNode(language) {
|
|
1041
|
+
const options = normalizeOptions(newStyleOptions, language);
|
|
1042
|
+
const eol = getEol(/** @type {LineEndingOption} */(options.lineEndings));
|
|
1043
|
+
const header = /** @type {InlineConfig} */ (options.header);
|
|
1044
|
+
const canFix = !header.lines.some((line) =>
|
|
1045
|
+
isPattern(/** @type {HeaderLine} */(line)) && !("template" in line));
|
|
1046
|
+
const fixLines = header.lines.map((line) => {
|
|
1047
|
+
if (isPattern(/** @type {HeaderLine} */(line))) {
|
|
1048
|
+
return ("template" in line) ? /** @type {string} */(line.template) : "";
|
|
1009
1049
|
}
|
|
1050
|
+
return /** @type {string} */(line);
|
|
1051
|
+
});
|
|
1052
|
+
const numLines = /** @type {number} */ (options.trailingEmptyLines?.minimum);
|
|
1010
1053
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (!commentMatched) {
|
|
1022
|
-
if ("messageId" in headerReport && headerReport.messageId === "noNewlineAfterHeader") {
|
|
1023
|
-
const { expected, actual } =
|
|
1024
|
-
/** @type {{ expected: number, actual: number }} */ (headerReport.data);
|
|
1025
|
-
headerReport.fix = genEmptyLinesFixer(leadingComment, eol, expected - actual);
|
|
1026
|
-
} else if (canFix) {
|
|
1027
|
-
headerReport.fix = genReplaceFixer(
|
|
1028
|
-
headerMatcher.commentType,
|
|
1029
|
-
sourceCode,
|
|
1030
|
-
leadingComment,
|
|
1031
|
-
fixLines,
|
|
1032
|
-
eol,
|
|
1033
|
-
numLines);
|
|
1034
|
-
}
|
|
1054
|
+
if (!language.lineComment && (/** @type {InlineConfig} */ (options.header)).commentType === "line") {
|
|
1055
|
+
context.report({
|
|
1056
|
+
messageId: "unsupportedLineHeader",
|
|
1057
|
+
loc: {
|
|
1058
|
+
start: sourceCode.getLocFromIndex(0),
|
|
1059
|
+
end: sourceCode.getLocFromIndex(1),
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1035
1064
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1065
|
+
const headerMatcher = new CommentMatcher(header, eol, numLines, language);
|
|
1066
|
+
const allowedLeadingComments = /** @type {LeadingComments} */ (options.leadingComments).comments;
|
|
1067
|
+
const allowedCommentsMatchers =
|
|
1068
|
+
allowedLeadingComments.map((c) => new CommentMatcher(/** @type {InlineConfig} */(c), eol, 0, language));
|
|
1069
|
+
|
|
1070
|
+
const leadingComments = getLeadingComments(sourceCode, language);
|
|
1071
|
+
// XXX: the reason we test like this instead of checking the
|
|
1072
|
+
// first comment is of type "Shabeng" is because
|
|
1073
|
+
// @typecript-eslint/prser does not recognize shebang comments
|
|
1074
|
+
// with some TypeScript configuration. Since we are not
|
|
1075
|
+
// releasing a major version we do not want to break the current
|
|
1076
|
+
// behavior of not be pedantic about the tsconfig.json.
|
|
1077
|
+
const hasShebang = language.shebang && sourceCode.text.startsWith("#!");
|
|
1078
|
+
let startingHeaderLine = 1;
|
|
1079
|
+
if (hasShebang) {
|
|
1080
|
+
if (leadingComments.length > 0
|
|
1081
|
+
&& /** @type {string} */ (leadingComments[0][0].type) === "Shebang"
|
|
1082
|
+
) {
|
|
1083
|
+
leadingComments.splice(0, 1);
|
|
1046
1084
|
}
|
|
1085
|
+
startingHeaderLine = 2;
|
|
1086
|
+
}
|
|
1047
1087
|
|
|
1048
|
-
|
|
1049
|
-
const lastCommentLine = lastComment[lastComment.length - 1];
|
|
1050
|
-
const lineIndex = /** @type {number} */ (lastCommentLine.loc?.end.line) + 1;
|
|
1051
|
-
|
|
1088
|
+
if (leadingComments.length === 0 || leadingComments[0][0].loc.start.line > startingHeaderLine) {
|
|
1052
1089
|
context.report({
|
|
1053
1090
|
loc: {
|
|
1054
1091
|
start: {
|
|
1055
1092
|
column: 0,
|
|
1056
|
-
line:
|
|
1093
|
+
line: startingHeaderLine
|
|
1057
1094
|
},
|
|
1058
1095
|
end: {
|
|
1059
1096
|
column: 0,
|
|
1060
|
-
line:
|
|
1097
|
+
line: startingHeaderLine
|
|
1061
1098
|
}
|
|
1062
1099
|
},
|
|
1063
1100
|
messageId: "missingHeader",
|
|
@@ -1067,12 +1104,121 @@ const headerRule = {
|
|
|
1067
1104
|
sourceCode,
|
|
1068
1105
|
fixLines,
|
|
1069
1106
|
eol,
|
|
1070
|
-
numLines
|
|
1107
|
+
numLines,
|
|
1108
|
+
language)
|
|
1071
1109
|
: null
|
|
1072
1110
|
});
|
|
1111
|
+
return;
|
|
1073
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
for (const leadingComment of leadingComments) {
|
|
1115
|
+
|
|
1116
|
+
const headerReport = headerMatcher.validate(leadingComment, sourceCode);
|
|
1117
|
+
if (headerReport === null) {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const leadingCommentReports =
|
|
1121
|
+
allowedCommentsMatchers.map((m) => m.validate(leadingComment, sourceCode));
|
|
1122
|
+
const commentMatched = leadingCommentReports.some((report) => report === null);
|
|
1123
|
+
|
|
1124
|
+
if (!commentMatched) {
|
|
1125
|
+
if ("messageId" in headerReport && headerReport.messageId === "noNewlineAfterHeader") {
|
|
1126
|
+
const { expected, actual } =
|
|
1127
|
+
/** @type {{ expected: number, actual: number }} */ (headerReport.data);
|
|
1128
|
+
headerReport.fix = genEmptyLinesFixer(leadingComment, eol, expected - actual);
|
|
1129
|
+
} else if (canFix) {
|
|
1130
|
+
headerReport.fix = genReplaceFixer(
|
|
1131
|
+
headerMatcher.commentType,
|
|
1132
|
+
sourceCode,
|
|
1133
|
+
leadingComment,
|
|
1134
|
+
fixLines,
|
|
1135
|
+
eol,
|
|
1136
|
+
numLines,
|
|
1137
|
+
language);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
context.report(headerReport);
|
|
1141
|
+
for (const commentReport of leadingCommentReports) {
|
|
1142
|
+
if (commentReport !== null) {
|
|
1143
|
+
/** @type {{ messageId: string }} */ (commentReport).messageId =
|
|
1144
|
+
"leadingComment-" + /** @type {{ messageId: string }} */ (commentReport).messageId;
|
|
1145
|
+
context.report(commentReport);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const lastComment = leadingComments[leadingComments.length - 1];
|
|
1153
|
+
const lastCommentLine = lastComment[lastComment.length - 1];
|
|
1154
|
+
const lineIndex = lastCommentLine.loc.end.line + 1;
|
|
1155
|
+
|
|
1156
|
+
context.report({
|
|
1157
|
+
loc: {
|
|
1158
|
+
start: {
|
|
1159
|
+
column: 0,
|
|
1160
|
+
line: lineIndex
|
|
1161
|
+
},
|
|
1162
|
+
end: {
|
|
1163
|
+
column: 0,
|
|
1164
|
+
line: lineIndex
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
messageId: "missingHeader",
|
|
1168
|
+
fix: canFix
|
|
1169
|
+
? genPrependFixer(
|
|
1170
|
+
headerMatcher.commentType,
|
|
1171
|
+
sourceCode,
|
|
1172
|
+
fixLines,
|
|
1173
|
+
eol,
|
|
1174
|
+
numLines,
|
|
1175
|
+
language)
|
|
1176
|
+
: null
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/** @type {Language} */
|
|
1181
|
+
const jsStyleLanguage = {
|
|
1182
|
+
blockComment: {
|
|
1183
|
+
startDelimiter: "/*",
|
|
1184
|
+
endDelimiter: "*/"
|
|
1185
|
+
},
|
|
1186
|
+
lineComment: {
|
|
1187
|
+
startDelimiter: "//"
|
|
1188
|
+
},
|
|
1189
|
+
shebang: true
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
/** @type {Language} */
|
|
1193
|
+
const cssStyleLanguage = {
|
|
1194
|
+
blockComment: {
|
|
1195
|
+
startDelimiter: "/*",
|
|
1196
|
+
endDelimiter: "*/"
|
|
1197
|
+
},
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
/** @type {Language} */
|
|
1201
|
+
const htmlStyleLanguage = {
|
|
1202
|
+
blockComment: {
|
|
1203
|
+
startDelimiter: "<!--",
|
|
1204
|
+
endDelimiter: "-->"
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const hooks = {
|
|
1209
|
+
Program: () =>
|
|
1210
|
+
onRootNode(
|
|
1211
|
+
sourceCode.constructor.name === "HTMLSourceCode"
|
|
1212
|
+
? htmlStyleLanguage
|
|
1213
|
+
: jsStyleLanguage
|
|
1214
|
+
),
|
|
1215
|
+
StyleSheet: () => onRootNode(cssStyleLanguage),
|
|
1216
|
+
// eslint/markdown
|
|
1217
|
+
root: () => onRootNode(htmlStyleLanguage),
|
|
1074
1218
|
};
|
|
1219
|
+
return hooks;
|
|
1075
1220
|
}
|
|
1076
1221
|
};
|
|
1077
1222
|
|
|
1078
1223
|
exports.header = headerRule;
|
|
1224
|
+
exports.parsedFilesCache = parsedFilesCache;
|