@tony.ganchev/eslint-plugin-header 3.3.3 → 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.
@@ -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
- * @returns {Comment[][]} Array of groups of leading comments.
222
+ * @param {Language} language The language configuration.
223
+ * @returns {CommentEx[][]} Array of groups of leading comments.
187
224
  */
188
- function getLeadingComments(sourceCode) {
189
- const all = sourceCode.getAllComments();
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 {Comment[]} */
228
+ /** @type {CommentEx[]} */
204
229
  let currentGroup = [];
205
- for (let i = 0; i < commentsBeforeCode.length; ++i) {
206
- const comment = commentsBeforeCode[i];
230
+ let pos = 0;
231
+ let lastCommentEnd = 0;
207
232
 
208
- if (comment.type === "Block" || /** @type {string} */ (comment.type) === "Shebang") {
209
- // Push any existing current group first
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
- if (currentGroup.length === 0) {
217
- currentGroup.push(comment);
218
- } else {
219
- const previous = currentGroup[currentGroup.length - 1];
220
- const previousRange = /** @type {[number, number]} */ (previous.range);
221
- const currentRange = /** @type {[number, number]} */ (comment.range);
222
- const txt = sourceCode.text.slice(previousRange[1], currentRange[0]);
223
-
224
- // If there is more than 1 newline, there is an empty line
225
- // between comments.
226
- const newlineCount = /** @type {RegExpMatchArray} */ (txt.match(/\r?\n/g)).length;
227
- if (newlineCount <= 1) {
228
- currentGroup.push(comment);
229
- } else {
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 = [comment];
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 "/*" + textArray.join(eol) + "*/";
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 "//" + textArray.join(eol + "//");
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 {Comment[]} comments List of comments.
266
- * @returns {[number, number]} Resulting range.
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 firstCommentRange = /** @type {[number, number]} */ (firstComment.range);
272
- const start = firstCommentRange[0];
369
+ const start = firstComment.range[0];
273
370
  const lastComment = comments.slice(-1)[0];
274
- const lastCommentRange = /** @type {[number, number]} */ (lastComment.range);
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 {Comment[]} leadingComments Comment elements to replace.
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 {Comment[]} leadingComments Comment elements to replace.
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
- const text = fs.readFileSync(matcher.file, matcher.encoding || "utf8");
493
- const [commentType, lines] = commentParser(text);
494
- return { commentType, lines };
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 {Comment[]} leadingComments The comment lines that constitute the
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 = /** @type {SourceLocation} */ (leadingComments[leadingComments.length - 1].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 {Comment[]} leadingComments The block comment or sequence of line
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 = /** @type {SourceLocation} */ (leadingComments[0].loc);
612
- const firstLeadingCommentRange = /** @type {[number, number]} */ (leadingComments[0].range);
719
+ const firstLeadingCommentLoc = leadingComments[0].loc;
720
+ const firstLeadingCommentRange = leadingComments[0].range;
613
721
 
614
- const lastLeadingCommentLoc = /** @type {SourceLocation} */ (leadingComments[leadingComments.length - 1].loc);
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 = /** @type {SourceLocation} */ (comment.loc);
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: "//".length + j,
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: "//".length + headerLineLength,
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: "//".length,
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 = /** @type {[number, number]} */ (leadingComments[this.headerLines.length - 1].range);
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 ? "/*".length : 0;
887
+ const columnOffset = i === 0 ? blockStartLen : 0;
776
888
  const line = firstLeadingCommentLoc.start.line + i;
777
889
  errorMessageLoc = {
778
- start: {
779
- column: columnOffset + j,
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 ? "/*".length : 0) + leadingLine.length;
903
+ const startColumn = (i === 0 ? blockStartLen : 0) + leadingLine.length;
799
904
  errorMessageLoc = {
800
- start: {
801
- column: startColumn,
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 ? "/*".length : 0) + headerLine.length,
915
+ column: (i === 0 ? blockStartLen : 0) + headerLine.length,
819
916
  line: firstLeadingCommentLoc.start.line + i
820
917
  },
821
918
  end: {
822
- column: (i === 0 ? "/*".length : 0) + leadingLine.length,
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
- pattern: headerLine.toString()
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
- column: columnOffset + 0,
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 ? "/*".length : 0) + 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 - "*/".length,
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 options = normalizeOptions(newStyleOptions);
943
- const eol = getEol(/** @type {LineEndingOption} */(options.lineEndings));
944
- const header = /** @type {InlineConfig} */ (options.header);
945
- const canFix = !header.lines.some((line) => isPattern(line) && !("template" in line));
946
- const fixLines = header.lines.map((line) => {
947
- if (isPattern(line)) {
948
- return ("template" in line) ? /** @type {string} */(line.template) : "";
949
- }
950
- return /** @type {string} */(line);
951
- });
952
- const numLines = /** @type {number} */ (options.trailingEmptyLines?.minimum);
953
- const headerMatcher = new CommentMatcher(header, eol, numLines);
954
- const allowedLeadingComments = /** @type {LeadingComments} */ (options.leadingComments).comments;
955
- const allowedCommentsMatchers =
956
- allowedLeadingComments.map((c) => new CommentMatcher(/** @type {InlineConfig} */(c), eol, 0));
957
-
958
- return {
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
- for (const leadingComment of leadingComments) {
1012
-
1013
- const headerReport = headerMatcher.validate(leadingComment, sourceCode);
1014
- if (headerReport === null) {
1015
- return;
1016
- }
1017
- const leadingCommentReports =
1018
- allowedCommentsMatchers.map((m) => m.validate(leadingComment, sourceCode));
1019
- const commentMatched = leadingCommentReports.some((report) => report === null);
1020
-
1021
- if (!commentMatched) {
1022
- if ("messageId" in headerReport && headerReport.messageId === "noNewlineAfterHeader") {
1023
- const { expected, actual } =
1024
- /** @type {{ expected: number, actual: number }} */ (headerReport.data);
1025
- headerReport.fix = genEmptyLinesFixer(leadingComment, eol, expected - actual);
1026
- } else if (canFix) {
1027
- headerReport.fix = genReplaceFixer(
1028
- headerMatcher.commentType,
1029
- sourceCode,
1030
- leadingComment,
1031
- fixLines,
1032
- eol,
1033
- numLines);
1034
- }
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
- context.report(headerReport);
1037
- for (const commentReport of leadingCommentReports) {
1038
- if (commentReport !== null) {
1039
- /** @type {{ messageId: string }} */ (commentReport).messageId =
1040
- "leadingComment-" + /** @type {{ messageId: string }} */ (commentReport).messageId;
1041
- context.report(commentReport);
1042
- }
1043
- }
1044
- return;
1045
- }
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
- const lastComment = leadingComments[leadingComments.length - 1];
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: lineIndex
1093
+ line: startingHeaderLine
1057
1094
  },
1058
1095
  end: {
1059
1096
  column: 0,
1060
- line: lineIndex
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;