@tony.ganchev/eslint-plugin-header 3.1.10 → 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,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright (c) 2015-present Stuart Knightley, Tony Ganchev and contributors
3
+ Copyright (c) 2015-present Stuart Knightley, Tony Ganchev, and contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the “Software”), to deal in
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * MIT License
3
3
  *
4
- * Copyright (c) 2015-present Stuart Knightley, Tony Ganchev 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
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * MIT License
3
3
  *
4
- * Copyright (c) 2015-present Stuart Knightley, Tony Ganchev 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
@@ -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,6 +40,9 @@
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
48
  /**
@@ -52,13 +60,6 @@
52
60
  * } HeaderOptions
53
61
  */
54
62
 
55
- const assert = require("assert");
56
- const fs = require("fs");
57
- const os = require("os");
58
- const commentParser = require("../comment-parser");
59
- const { description, recommended, url } = require("./header.docs");
60
- const { commentTypeOptions, lineEndingOptions, schema } = require("./header.schema");
61
-
62
63
  /**
63
64
  * Tests if the passed line configuration string or object is a pattern
64
65
  * definition.
@@ -90,8 +91,8 @@ function match(actual, expected) {
90
91
  * Remove Unix she-bangs from the list of comments.
91
92
  * @param {Comment[]} comments the list of comment lines.
92
93
  * @returns {Comment[]} the list of comments with containing all incomming
93
- * comments from `comments` with the shebang
94
- * comments omitted.
94
+ * comments from `comments` with the shebang comments
95
+ * omitted.
95
96
  */
96
97
  function excludeShebangs(comments) {
97
98
  return comments.filter(function(comment) {
@@ -186,7 +187,7 @@ function genPrependFixer(commentType, context, headerLines, eol, numNewlines) {
186
187
  return function(fixer) {
187
188
  let insertPos = 0;
188
189
  let newHeader = genCommentBody(commentType, headerLines, eol, numNewlines);
189
- if (context.sourceCode.text.substring(0, 2) === "#!") {
190
+ if (context.sourceCode.text.startsWith("#!")) {
190
191
  const firstNewLinePos = context.sourceCode.text.indexOf("\n");
191
192
  insertPos = firstNewLinePos === -1 ? context.sourceCode.text.length : firstNewLinePos + 1;
192
193
  if (firstNewLinePos === -1) {
@@ -288,16 +289,33 @@ function getEOL(options) {
288
289
  * @param {string} src source code to test.
289
290
  * @returns {boolean} `true` if there is a comment or `false` otherwise.
290
291
  */
291
- // TODO: check if it is valid to have the copyright notice separated by an empty
292
- // line from the shebang.
293
292
  function hasHeader(src) {
294
- if (src.startsWith("#!")) {
295
- const m = src.match(/(\r\n|\r|\n)/);
296
- if (m) {
297
- src = src.slice(m.index + m[0].length);
293
+ const srcWithoutShebang = src.replace(/^#![^\n]*\r?\n/, "");
294
+ return srcWithoutShebang.startsWith("/*") || srcWithoutShebang.startsWith("//");
295
+ }
296
+
297
+ /**
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.
309
+ */
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
298
317
  }
299
- }
300
- return src.startsWith("/*") || src.startsWith("//");
318
+ };
301
319
  }
302
320
 
303
321
  module.exports = {
@@ -312,10 +330,16 @@ module.exports = {
312
330
  schema,
313
331
  defaultOptions: [{}],
314
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",
315
338
  incorrectCommentType: "header should be a {{commentType}} comment",
316
339
  incorrectHeader: "incorrect header",
340
+ incorrectHeaderLine: "header line does not match pattern: {{pattern}}",
317
341
  missingHeader: "missing header",
318
- noNewlineAfterHeader: "no newline after header"
342
+ noNewlineAfterHeader: "not enough newlines after header: expected: {{expected}}, actual: {{actual}}"
319
343
  }
320
344
  },
321
345
  /**
@@ -335,6 +359,7 @@ module.exports = {
335
359
  }
336
360
 
337
361
  const commentType = options[0].toLowerCase();
362
+ /** @type {(string | RegExp)[]} */
338
363
  let headerLines;
339
364
  let fixLines = [];
340
365
  // If any of the lines are regular expressions, then we can't
@@ -373,8 +398,19 @@ module.exports = {
373
398
  */
374
399
  Program: function(node) {
375
400
  if (!hasHeader(context.sourceCode.text)) {
401
+ const hasShebang = context.sourceCode.text.startsWith("#!");
402
+ const line = hasShebang ? 2 : 1;
376
403
  context.report({
377
- loc: node.loc,
404
+ loc: {
405
+ start: {
406
+ column: 1,
407
+ line
408
+ },
409
+ end: {
410
+ column: 1,
411
+ line
412
+ }
413
+ },
378
414
  messageId: "missingHeader",
379
415
  fix: genPrependFixer(commentType, context, fixLines, eol, numNewlines)
380
416
  });
@@ -384,7 +420,10 @@ module.exports = {
384
420
 
385
421
  if (leadingComments[0].type.toLowerCase() !== commentType) {
386
422
  context.report({
387
- loc: node.loc,
423
+ loc: {
424
+ start: leadingComments[0].loc.start,
425
+ end: leadingComments[leadingComments.length - 1].loc.end
426
+ },
388
427
  messageId: "incorrectCommentType",
389
428
  data: {
390
429
  commentType: commentType
@@ -396,16 +435,6 @@ module.exports = {
396
435
  return;
397
436
  }
398
437
  if (commentType === commentTypeOptions.line) {
399
- if (leadingComments.length < headerLines.length) {
400
- context.report({
401
- loc: node.loc,
402
- messageId: "incorrectHeader",
403
- fix: canFix
404
- ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
405
- : null
406
- });
407
- return;
408
- }
409
438
  if (headerLines.length === 1) {
410
439
  const leadingCommentValues = leadingComments.map((c) => c.value);
411
440
  if (
@@ -413,7 +442,10 @@ module.exports = {
413
442
  && !match(leadingCommentValues.join("\r\n"), headerLines[0])
414
443
  ) {
415
444
  context.report({
416
- loc: node.loc,
445
+ loc: {
446
+ start: leadingComments[0].loc.start,
447
+ end: leadingComments[leadingComments.length - 1].loc.end
448
+ },
417
449
  messageId: "incorrectHeader",
418
450
  fix: canFix
419
451
  ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
@@ -423,10 +455,15 @@ module.exports = {
423
455
  }
424
456
  } else {
425
457
  for (let i = 0; i < headerLines.length; i++) {
426
- if (!match(leadingComments[i].value, headerLines[i])) {
458
+ if (leadingComments.length - 1 < i) {
427
459
  context.report({
428
- loc: node.loc,
429
- messageId: "incorrectHeader",
460
+ loc: {
461
+ start: leadingComments[leadingComments.length - 1].loc.end
462
+ },
463
+ messageId: "headerTooShort",
464
+ data: {
465
+ remainder: headerLines.slice(i).join(eol)
466
+ },
430
467
  fix: canFix
431
468
  ? genReplaceFixer(
432
469
  commentType,
@@ -439,6 +476,104 @@ module.exports = {
439
476
  });
440
477
  return;
441
478
  }
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) {
529
+ context.report({
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
547
+ });
548
+ return;
549
+ }
550
+ } else {
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;
575
+ }
576
+ }
442
577
  }
443
578
  }
444
579
 
@@ -447,9 +582,13 @@ module.exports = {
447
582
  const missingEmptyLines = numNewlines - actualLeadingEmptyLines;
448
583
  if (missingEmptyLines > 0) {
449
584
  context.report({
450
- loc: node.loc,
585
+ loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
451
586
  messageId: "noNewlineAfterHeader",
452
- fix: canFix ? genEmptyLinesFixer(leadingComments, eol, missingEmptyLines) : null
587
+ data: {
588
+ expected: numNewlines,
589
+ actual: actualLeadingEmptyLines
590
+ },
591
+ fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
453
592
  });
454
593
  }
455
594
  return;
@@ -461,26 +600,116 @@ module.exports = {
461
600
  leadingLines = leadingComments[0].value.split(/\r?\n/);
462
601
  }
463
602
 
464
- let hasError = false;
465
- if (leadingLines.length > headerLines.length) {
466
- hasError = true;
467
- }
468
- for (let i = 0; !hasError && i < headerLines.length; i++) {
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++) {
469
610
  const leadingLine = leadingLines[i];
470
611
  const headerLine = headerLines[i];
471
- if (!match(leadingLine, headerLine)) {
472
- hasError = true;
473
- break;
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;
632
+ }
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
648
+ }
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
665
+ }
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
684
+ }
685
+ };
686
+ break;
687
+ }
474
688
  }
475
689
  }
476
690
 
477
- if (hasError) {
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
701
+ }
702
+ };
703
+ }
704
+
705
+ if (errorMessageId) {
478
706
  if (canFix && headerLines.length > 1) {
479
707
  fixLines = [fixLines.join(eol)];
480
708
  }
481
709
  context.report({
482
- loc: node.loc,
483
- messageId: "incorrectHeader",
710
+ loc: errorMessageLoc,
711
+ messageId: errorMessageId,
712
+ data: errorMessageData,
484
713
  fix: canFix
485
714
  ? genReplaceFixer(commentType, context, leadingComments, fixLines, eol, numNewlines)
486
715
  : null
@@ -493,9 +722,13 @@ module.exports = {
493
722
  const missingEmptyLines = numNewlines - actualLeadingEmptyLines;
494
723
  if (missingEmptyLines > 0) {
495
724
  context.report({
496
- loc: node.loc,
725
+ loc: missingEmptyLinesViolationLoc(leadingComments, actualLeadingEmptyLines),
497
726
  messageId: "noNewlineAfterHeader",
498
- fix: canFix ? genEmptyLinesFixer(leadingComments, eol, missingEmptyLines) : null
727
+ data: {
728
+ expected: numNewlines,
729
+ actual: actualLeadingEmptyLines
730
+ },
731
+ fix: genEmptyLinesFixer(leadingComments, eol, missingEmptyLines)
499
732
  });
500
733
  }
501
734
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tony.ganchev/eslint-plugin-header",
3
- "version": "3.1.10",
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": [
@@ -16,14 +16,14 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "@eslint/eslintrc": "^3.3.1",
19
- "@eslint/js": "^9.32.0",
19
+ "@eslint/js": "^9.39.1",
20
20
  "@eslint/markdown": "^7.5.1",
21
21
  "@stylistic/eslint-plugin": "^5.5.0",
22
- "eslint": "^9.32.0",
22
+ "eslint": "^9.39.1",
23
23
  "eslint-plugin-eslint-plugin": "^7.2.0",
24
- "eslint-plugin-jsdoc": "^52.0.4",
24
+ "eslint-plugin-jsdoc": "^61.1.12",
25
25
  "eslint-plugin-n": "^17.23.1",
26
- "mocha": "^11.7.1",
26
+ "mocha": "^11.7.5",
27
27
  "nyc": "^17.1.0",
28
28
  "testdouble": "^3.20.2"
29
29
  },