@lewishowles/lint-config 0.4.0 → 0.6.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.
@@ -1,4 +1,4 @@
1
- import { getDocumentationNode } from "../utils/documentation.js";
1
+ import { getDocumentationNode, isFunctionValue } from "../utils/documentation.js";
2
2
  import { hasImmediateLineComment } from "../utils/source.js";
3
3
 
4
4
  // Declaration kinds that require an immediately preceding line comment.
@@ -33,6 +33,18 @@ export default {
33
33
  meta: {
34
34
  docs: { description: "Require comments before variable declarations." },
35
35
  type: "suggestion",
36
+ schema: [
37
+ {
38
+ type: "object",
39
+ properties: {
40
+ rootOnly: {
41
+ type: "boolean",
42
+ },
43
+ },
44
+ additionalProperties: false,
45
+ },
46
+ ],
47
+ defaultOptions: [{ rootOnly: false }],
36
48
  },
37
49
  /**
38
50
  * Create the rule's node visitors.
@@ -56,7 +68,8 @@ export default {
56
68
  return;
57
69
  }
58
70
 
59
- // A lone const class expression is documented by class-documentation.
71
+ // A lone const class expression is documented by
72
+ // class-documentation.
60
73
  if (
61
74
  node.kind === "const" &&
62
75
  node.declarations.length === 1 &&
@@ -66,10 +79,34 @@ export default {
66
79
  return;
67
80
  }
68
81
 
69
- // Resolves any export wrapper before checking for documentation.
82
+ // Whether every name in this const holds a function. The
83
+ // function-documentation rule already requires a JSDoc block on
84
+ // those, so asking for a line comment as well would make the
85
+ // two rules impossible to satisfy together.
86
+ const isFunctionValuedConst =
87
+ node.kind === "const" &&
88
+ node.declarations.every(
89
+ ({ id, init }) => id.type === "Identifier" && isFunctionValue(init),
90
+ );
91
+
92
+ // The rule's resolved options for the file currently being
93
+ // visited.
94
+ const options = context.options?.[0];
95
+ // Resolves any export wrapper before checking for
96
+ // documentation.
70
97
  const documentationNode = getDocumentationNode(node);
71
98
 
72
- if (!hasImmediateLineComment(context.sourceCode, documentationNode)) {
99
+ // With rootOnly, only declarations directly under the Program
100
+ // need a comment, so a nested variable inside a function is
101
+ // left alone.
102
+ const shouldCheckDocumentation =
103
+ !options?.rootOnly || documentationNode.parent?.type === "Program";
104
+
105
+ if (
106
+ !isFunctionValuedConst &&
107
+ shouldCheckDocumentation &&
108
+ !hasImmediateLineComment(context.sourceCode, documentationNode)
109
+ ) {
73
110
  context.report({
74
111
  message: "Variable declarations require an immediately preceding line comment.",
75
112
  node: documentationNode,
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { isDirectiveComment } from "../utils/source.js";
3
3
 
4
- // Captures the opening tag's attributes separately, so the setup attribute
5
- // can be tested without the tag being available from Oxlint's extracted AST.
4
+ // Captures the opening tag's attributes separately, so the setup attribute can
5
+ // be tested without the tag being available from Oxlint's extracted AST.
6
6
  const scriptBlockPattern =
7
7
  /(?<openingTag><script\b(?<attributes>[^>]*)>)(?<content>[\s\S]*?)<\/script\s*>/gi;
8
8
 
@@ -69,8 +69,8 @@ function getScriptBlocks(context) {
69
69
  * The matching raw script block, when one is found.
70
70
  */
71
71
  function findScriptBlock(context) {
72
- // Re-read on every call rather than caching, since this file's other
73
- // script blocks may not have been visited yet or ever in this run.
72
+ // Re-read on every call rather than caching, since this file's other script
73
+ // blocks may not have been visited yet or ever in this run.
74
74
  const scriptBlocks = getScriptBlocks(context);
75
75
 
76
76
  return scriptBlocks.find(
@@ -119,7 +119,8 @@ function hasComponentDocumentation(context, scriptBlock) {
119
119
  return false;
120
120
  }
121
121
 
122
- // A blank line would separate the component documentation from its entry point.
122
+ // A blank line would separate the component documentation from its entry
123
+ // point.
123
124
  const commentGap = context.sourceCode.text.slice(0, comment.range[0]);
124
125
 
125
126
  return immediateCommentGapPattern.test(commentGap);
@@ -142,7 +143,8 @@ export default {
142
143
  createOnce(context) {
143
144
  return {
144
145
  /**
145
- * Check the current component's script setup block for documentation.
146
+ * Check the current component's script setup block for
147
+ * documentation.
146
148
  *
147
149
  * @param {object} node
148
150
  * The entry point parsed from the current script block.
@@ -150,8 +152,8 @@ export default {
150
152
  Program(node) {
151
153
  // createOnce builds this visitor once for the whole run, and a
152
154
  // single file's script blocks are not necessarily visited
153
- // consecutively, so the matching block is looked up fresh on each
154
- // call rather than tracked with shared state.
155
+ // consecutively, so the matching block is looked up fresh on
156
+ // each call rather than tracked with shared state.
155
157
  const scriptBlock = findScriptBlock(context);
156
158
 
157
159
  if (hasComponentDocumentation(context, scriptBlock)) {
@@ -111,7 +111,8 @@ export default {
111
111
  return;
112
112
  }
113
113
 
114
- // Array and type-only forms have no runtime properties to document.
114
+ // Array and type-only forms have no runtime properties to
115
+ // document.
115
116
  const emitsObject = getObjectArgument(node, 0);
116
117
 
117
118
  if (emitsObject) {
@@ -293,7 +293,6 @@ function hasValueReturn(node) {
293
293
  export function reportFunctionDocumentation(context, node, functionNode, options = {}) {
294
294
  // Defaults to "Functions" when the caller names no declaration kind.
295
295
  const subject = options.subject ?? "Functions";
296
-
297
296
  // Finds the JSDoc block documenting this function, when present.
298
297
  const comment = getDocumentationComment(context.sourceCode, node);
299
298
 
@@ -1,5 +1,11 @@
1
- import { getCommentText, getLineIndent, getNewline } from "./source.js";
2
- import { addTerminalPunctuation, capitaliseSentence, formatSentence, wrapWords } from "./wrap.js";
1
+ import { getDisplayWidth } from "./source.js";
2
+ import {
3
+ addTerminalPunctuation,
4
+ capitaliseSentence,
5
+ formatSentence,
6
+ refillCommentLines,
7
+ wrapWords,
8
+ } from "./wrap.js";
3
9
 
4
10
  // The JSDoc tags this package formats, in their required output order.
5
11
  const tagOrder = ["param", "throws", "returns"];
@@ -148,6 +154,7 @@ function parseTargetTags(tagLines) {
148
154
  rest: match[2].trim(),
149
155
  type: match[1],
150
156
  };
157
+
151
158
  entries.push(currentEntry);
152
159
  } else if (currentEntry) {
153
160
  currentEntry.description.push(line);
@@ -203,21 +210,27 @@ function getInlineTagDescription(entry) {
203
210
  }
204
211
 
205
212
  /**
206
- * Format prose without changing its existing line wrapping.
213
+ * Format and refill JSDoc prose while preserving paragraph and list boundaries.
207
214
  *
208
215
  * @param {string[]} lines
209
216
  * The prose content lines.
210
217
  * @param {boolean} addPunctuation
211
218
  * Whether to format each paragraph as a sentence.
219
+ * @param {string} indentation
220
+ * The indentation used by the comment.
212
221
  *
213
222
  * @returns {string[]}
214
223
  * The formatted prose lines.
215
224
  */
216
- function formatUnwrappedProse(lines, addPunctuation) {
217
- // The formatted lines, built up in place.
218
- const result = lines.map((line) => line.trim());
219
-
220
- // The index of the current paragraph's first line, or null between paragraphs.
225
+ function formatUnwrappedProse(lines, addPunctuation, indentation) {
226
+ // The prose lines after refilling, then formatted in place below.
227
+ const result = refillCommentLines(
228
+ lines.map((line) => ({ prefix: `${indentation} * `, text: line })),
229
+ 80,
230
+ ).map(({ text }) => text);
231
+
232
+ // The index of the current paragraph's first line, or null between
233
+ // paragraphs.
221
234
  let paragraphStart = null;
222
235
 
223
236
  for (let index = 0; index < result.length; index += 1) {
@@ -281,12 +294,14 @@ function formatProse(lines, width, addPunctuation) {
281
294
  }
282
295
 
283
296
  result.push(...wrapWords(text, width));
297
+
284
298
  paragraph = [];
285
299
  }
286
300
 
287
301
  for (const line of lines) {
288
302
  if (line.trim() === "") {
289
303
  flushParagraph();
304
+
290
305
  if (result.at(-1) !== "") {
291
306
  result.push("");
292
307
  }
@@ -336,7 +351,8 @@ function formatTags(tagLines, width, addPunctuation, normaliseTags) {
336
351
  // The formatted lines, built up in place.
337
352
  const result = [];
338
353
 
339
- // The tag type of the previously written entry, used to detect group changes.
354
+ // The tag type of the previously written entry, used to detect group
355
+ // changes.
340
356
  let lastType = null;
341
357
 
342
358
  // The entries regrouped into the required tag order.
@@ -406,15 +422,18 @@ function formatMixedTags(lines, width, addPunctuation) {
406
422
  type: match[1],
407
423
  };
408
424
  preserveSection = false;
425
+
409
426
  result.push(formatTagHeader(currentEntry));
410
427
  } else if (tagName !== null) {
411
428
  currentEntry = null;
412
429
  preserveSection = isPreservedSectionTag(line);
430
+
413
431
  result.push(line.trim());
414
432
  } else if (preserveSection) {
415
433
  result.push(line);
416
434
  } else if (line.trim() === "") {
417
435
  currentEntry = null;
436
+
418
437
  if (result.at(-1) !== "") {
419
438
  result.push("");
420
439
  }
@@ -475,6 +494,7 @@ function formatTagDescriptions(lines, width, addPunctuation) {
475
494
  }
476
495
 
477
496
  result.push(...wrapWords(text, width).map((line) => ` ${line}`));
497
+
478
498
  description = [];
479
499
  }
480
500
 
@@ -485,11 +505,13 @@ function formatTagDescriptions(lines, width, addPunctuation) {
485
505
  if (tagName !== null) {
486
506
  flushDescription();
487
507
  result.push(line.trim());
508
+
488
509
  preserveSection = isPreservedSectionTag(line);
489
510
  } else if (preserveSection) {
490
511
  result.push(line);
491
512
  } else if (line.trim() === "") {
492
513
  flushDescription();
514
+
493
515
  if (result.at(-1) !== "") {
494
516
  result.push("");
495
517
  }
@@ -510,23 +532,20 @@ function formatTagDescriptions(lines, width, addPunctuation) {
510
532
  /**
511
533
  * Return the details needed to format a JSDoc block comment.
512
534
  *
513
- * @param {object} sourceCode
514
- * The Oxlint source code object.
515
- * @param {object} comment
516
- * The JSDoc comment token.
535
+ * @param {string} commentText
536
+ * The JSDoc comment text.
537
+ * @param {object} formattingOptions
538
+ * The indentation (`indent`) and newline style (`newline`) of the source
539
+ * around the comment.
517
540
  *
518
541
  * @returns {object}
519
542
  * The JSDoc content and layout details.
520
543
  */
521
- function getJSDocFormattingContext(sourceCode, comment) {
522
- // The raw comment source text.
523
- const commentText = getCommentText(sourceCode, comment);
524
- // The indentation the comment's lines are aligned to.
525
- const indent = getLineIndent(sourceCode, comment.range[0]) ?? "";
526
- // The newline style used by the surrounding source.
527
- const newline = getNewline(sourceCode.text);
544
+ function getJSDocFormattingContext(commentText, formattingOptions) {
545
+ // The indentation and newline style used by the surrounding source.
546
+ const { indent, newline } = formattingOptions;
528
547
  // The available content width, allowing for the indent and " * " prefix.
529
- const width = Math.max(1, 80 - indent.length - 3);
548
+ const width = Math.max(1, 80 - getDisplayWidth(indent) - 3);
530
549
  // The undecorated comment content lines.
531
550
  const content = getJSDocContent(commentText);
532
551
  // The content split into its prose and tag sections.
@@ -611,19 +630,20 @@ function renderJSDocComment(formattingContext, outputLines) {
611
630
  /**
612
631
  * Format JSDoc block structure and delimiters.
613
632
  *
614
- * @param {object} sourceCode
615
- * The Oxlint source code object.
616
- * @param {object} comment
617
- * The JSDoc comment token.
633
+ * @param {string} commentText
634
+ * The JSDoc comment text.
635
+ * @param {object} formattingOptions
636
+ * The indentation (`indent`) and newline style (`newline`) of the source
637
+ * around the comment.
618
638
  *
619
639
  * @returns {string}
620
640
  * The formatted comment text.
621
641
  */
622
- export function formatJSDocBlockStructure(sourceCode, comment) {
642
+ export function formatJSDocBlockStructure(commentText, formattingOptions) {
623
643
  // The comment's parsed content and layout details.
624
- const formattingContext = getJSDocFormattingContext(sourceCode, comment);
625
- // The prose lines, rewrapped without changing existing line breaks.
626
- const prose = formatUnwrappedProse(formattingContext.proseLines, false);
644
+ const formattingContext = getJSDocFormattingContext(commentText, formattingOptions);
645
+ // The prose lines, refilled before tags are appended.
646
+ const prose = formatUnwrappedProse(formattingContext.proseLines, false, formattingContext.indent);
627
647
 
628
648
  // The tag lines, without spacing or grouping normalisation.
629
649
  const tags = formatTags(
@@ -641,20 +661,34 @@ export function formatJSDocBlockStructure(sourceCode, comment) {
641
661
  return renderJSDocComment(formattingContext, outputLines);
642
662
  }
643
663
 
664
+ /**
665
+ * Return whether a JSDoc comment contains a @param, @throws, or @returns tag.
666
+ *
667
+ * @param {string} commentText
668
+ * The JSDoc comment text.
669
+ *
670
+ * @returns {boolean}
671
+ * Whether one of those tags is present.
672
+ */
673
+ export function hasTargetJSDocTag(commentText) {
674
+ return getJSDocContent(commentText).some(isTargetTag);
675
+ }
676
+
644
677
  /**
645
678
  * Format JSDoc tag spacing, order, and grouping.
646
679
  *
647
- * @param {object} sourceCode
648
- * The Oxlint source code object.
649
- * @param {object} comment
650
- * The JSDoc comment token.
680
+ * @param {string} commentText
681
+ * The JSDoc comment text.
682
+ * @param {object} formattingOptions
683
+ * The indentation (`indent`) and newline style (`newline`) of the source
684
+ * around the comment.
651
685
  *
652
686
  * @returns {string}
653
687
  * The formatted comment text.
654
688
  */
655
- export function formatJSDocTagFormatting(sourceCode, comment) {
689
+ export function formatJSDocTagFormatting(commentText, formattingOptions) {
656
690
  // The comment's parsed content and layout details.
657
- const formattingContext = getJSDocFormattingContext(sourceCode, comment);
691
+ const formattingContext = getJSDocFormattingContext(commentText, formattingOptions);
658
692
  // The prose, rewrapped to the comment's available width.
659
693
  const prose = formatProse(formattingContext.proseLines, formattingContext.width, false);
660
694
 
@@ -677,19 +711,20 @@ export function formatJSDocTagFormatting(sourceCode, comment) {
677
711
  /**
678
712
  * Format JSDoc sentence capitalisation and punctuation.
679
713
  *
680
- * @param {object} sourceCode
681
- * The Oxlint source code object.
682
- * @param {object} comment
683
- * The JSDoc comment token.
714
+ * @param {string} commentText
715
+ * The JSDoc comment text.
716
+ * @param {object} formattingOptions
717
+ * The indentation (`indent`) and newline style (`newline`) of the source
718
+ * around the comment.
684
719
  *
685
720
  * @returns {string}
686
721
  * The formatted comment text.
687
722
  */
688
- export function formatJSDocPunctuation(sourceCode, comment) {
723
+ export function formatJSDocPunctuation(commentText, formattingOptions) {
689
724
  // The comment's parsed content and layout details.
690
- const formattingContext = getJSDocFormattingContext(sourceCode, comment);
725
+ const formattingContext = getJSDocFormattingContext(commentText, formattingOptions);
691
726
  // The prose, capitalised and punctuated as sentences.
692
- const prose = formatUnwrappedProse(formattingContext.proseLines, true);
727
+ const prose = formatUnwrappedProse(formattingContext.proseLines, true, formattingContext.indent);
693
728
 
694
729
  // The tag lines, with descriptions punctuated as sentences.
695
730
  const tags = formatTags(
@@ -710,17 +745,18 @@ export function formatJSDocPunctuation(sourceCode, comment) {
710
745
  /**
711
746
  * Format JSDoc prose and tags to the configured line width.
712
747
  *
713
- * @param {object} sourceCode
714
- * The Oxlint source code object.
715
- * @param {object} comment
716
- * The JSDoc comment token.
748
+ * @param {string} commentText
749
+ * The JSDoc comment text.
750
+ * @param {object} formattingOptions
751
+ * The indentation (`indent`) and newline style (`newline`) of the source
752
+ * around the comment.
717
753
  *
718
754
  * @returns {string}
719
755
  * The formatted comment text.
720
756
  */
721
- export function formatJSDocWrapping(sourceCode, comment) {
757
+ export function formatJSDocWrapping(commentText, formattingOptions) {
722
758
  // The comment's parsed content and layout details.
723
- const formattingContext = getJSDocFormattingContext(sourceCode, comment);
759
+ const formattingContext = getJSDocFormattingContext(commentText, formattingOptions);
724
760
  // The prose, rewrapped to the comment's available width.
725
761
  const prose = formatProse(formattingContext.proseLines, formattingContext.width, false);
726
762
 
@@ -739,18 +775,3 @@ export function formatJSDocWrapping(sourceCode, comment) {
739
775
 
740
776
  return renderJSDocComment(formattingContext, outputLines);
741
777
  }
742
-
743
- /**
744
- * Return whether a JSDoc comment contains a target tag.
745
- *
746
- * @param {object} sourceCode
747
- * The Oxlint source code object.
748
- * @param {object} comment
749
- * The comment token.
750
- *
751
- * @returns {boolean}
752
- * Whether a Phase 1 tag is present.
753
- */
754
- export function hasTargetJSDocTag(sourceCode, comment) {
755
- return getJSDocContent(getCommentText(sourceCode, comment)).some(isTargetTag);
756
- }
@@ -156,6 +156,21 @@ export function getLineIndent(sourceCode, offset) {
156
156
  return /^\s*$/.test(prefix) ? prefix : null;
157
157
  }
158
158
 
159
+ /**
160
+ * Return the number of columns a source string occupies on screen, so comment
161
+ * lines are measured and wrapped against the 80-column limit under tab
162
+ * indentation. A tab counts as four columns, the width this package assumes.
163
+ *
164
+ * @param {string} sourceText
165
+ * The source string to measure, such as a line or its indentation.
166
+ *
167
+ * @returns {number}
168
+ * The source string's width in display columns.
169
+ */
170
+ export function getDisplayWidth(sourceText) {
171
+ return sourceText.replaceAll("\t", " ").length;
172
+ }
173
+
159
174
  /**
160
175
  * Return the source items immediately around a comment.
161
176
  *
@@ -300,7 +315,6 @@ export function hasImmediateLineComment(sourceCode, node) {
300
315
 
301
316
  // Checks the comments immediately around the node.
302
317
  const { next, previous } = getCommentNeighbours(sourceCode, comment);
303
-
304
318
  // Confirms there is no blank line before the node.
305
319
  const gap = sourceCode.text.slice(comment.range[1], node.range[0]);
306
320
 
@@ -334,7 +348,6 @@ export function hasImmediateBlockComment(sourceCode, node) {
334
348
 
335
349
  // Checks the comments immediately around the node.
336
350
  const { next, previous } = getCommentNeighbours(sourceCode, comment);
337
-
338
351
  // Confirms there is no blank line before the node.
339
352
  const gap = sourceCode.text.slice(comment.range[1], node.range[0]);
340
353
 
@@ -1,3 +1,5 @@
1
+ import { getDisplayWidth } from "./source.js";
2
+
1
3
  /**
2
4
  * Wrap words to a maximum line width.
3
5
  *
@@ -23,6 +25,7 @@ export function wrapWords(text, width) {
23
25
  for (let index = 0; index < word.length; index += width) {
24
26
  lines.push(word.slice(index, index + width));
25
27
  }
28
+
26
29
  continue;
27
30
  }
28
31
 
@@ -32,6 +35,7 @@ export function wrapWords(text, width) {
32
35
  currentLine += ` ${word}`;
33
36
  } else {
34
37
  lines.push(currentLine);
38
+
35
39
  currentLine = word;
36
40
  }
37
41
  }
@@ -43,6 +47,123 @@ export function wrapWords(text, width) {
43
47
  return lines;
44
48
  }
45
49
 
50
+ /**
51
+ * Refill comment lines when the next line starts early enough to fit.
52
+ *
53
+ * @param {object[]} lines
54
+ * The comment lines, each with a display prefix and undecorated text.
55
+ * @param {number} maximumLineLength
56
+ * The maximum display width for a complete comment line.
57
+ *
58
+ * @returns {object[]}
59
+ * The refilled comment lines.
60
+ */
61
+ export function refillCommentLines(lines, maximumLineLength) {
62
+ // The lines copied so refilling does not change the caller's values.
63
+ const result = lines.map(({ prefix, text }) => ({ prefix, text: text.trimEnd() }));
64
+
65
+ for (let index = 0; index < result.length - 1; index += 1) {
66
+ // The current line being considered for refilling.
67
+ const currentLine = result[index];
68
+
69
+ while (index < result.length - 1) {
70
+ // The following line being considered for refilling.
71
+ const nextLine = result[index + 1];
72
+
73
+ if (!canRefillLine(currentLine.text) || !canProvideWords(nextLine.text)) {
74
+ break;
75
+ }
76
+
77
+ // The words still waiting on the following line.
78
+ const nextWords = nextLine.text.split(/\s+/).filter(Boolean);
79
+
80
+ // Whether the following line was removed after giving up all its
81
+ // words.
82
+ let removedNextLine = false;
83
+
84
+ while (nextWords.length > 0) {
85
+ // The first word that could move onto the current line.
86
+ const nextWord = nextWords[0];
87
+ // The candidate line after moving the next word.
88
+ const candidate = `${currentLine.prefix}${currentLine.text} ${nextWord}`;
89
+
90
+ if (getDisplayWidth(candidate) > maximumLineLength) {
91
+ break;
92
+ }
93
+
94
+ currentLine.text = `${currentLine.text} ${nextWord}`;
95
+
96
+ nextWords.shift();
97
+
98
+ nextLine.text = nextWords.join(" ");
99
+
100
+ if (nextLine.text === "") {
101
+ result.splice(index + 1, 1);
102
+
103
+ removedNextLine = true;
104
+
105
+ break;
106
+ }
107
+
108
+ if (!canRefillLine(currentLine.text)) {
109
+ break;
110
+ }
111
+ }
112
+
113
+ if (!removedNextLine) {
114
+ break;
115
+ }
116
+ }
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * Return whether a line may receive words from the next line.
124
+ *
125
+ * @param {string} text
126
+ * The undecorated comment text.
127
+ *
128
+ * @returns {boolean}
129
+ * Whether the line can be refilled.
130
+ */
131
+ function canRefillLine(text) {
132
+ // The line's text without its surrounding whitespace.
133
+ const trimmedText = text.trim();
134
+
135
+ return trimmedText !== "" && !/[.!?]$/.test(trimmedText) && !isListItem(trimmedText);
136
+ }
137
+
138
+ /**
139
+ * Return whether a line may provide words to the line above it.
140
+ *
141
+ * @param {string} text
142
+ * The undecorated comment text.
143
+ *
144
+ * @returns {boolean}
145
+ * Whether the line can provide its first word.
146
+ */
147
+ function canProvideWords(text) {
148
+ // The line's text without its surrounding whitespace.
149
+ const trimmedText = text.trim();
150
+
151
+ return trimmedText !== "" && !isListItem(trimmedText);
152
+ }
153
+
154
+ /**
155
+ * Return whether text starts with a list-item marker.
156
+ *
157
+ * @param {string} text
158
+ * The undecorated comment text.
159
+ *
160
+ * @returns {boolean}
161
+ * Whether the text is a list item.
162
+ */
163
+ function isListItem(text) {
164
+ return /^(?:[-*]\s+|\d+\.\s+)/.test(text.trim());
165
+ }
166
+
46
167
  /**
47
168
  * Capitalise a sentence and ensure it has terminal punctuation.
48
169
  *
@@ -83,8 +204,8 @@ export function capitaliseSentence(text) {
83
204
  // The leading word, starting from the first letter character.
84
205
  const leadingWord = trimmedText.slice(firstLetter).match(/^\p{L}[\p{L}\p{N}]*/u)?.[0] ?? "";
85
206
 
86
- // A camelCase word (lowercase start, later uppercase) is a code
87
- // identifier and must keep its own casing rather than sentence casing.
207
+ // A camelCase word (lowercase start, later uppercase) is a code identifier
208
+ // and must keep its own casing rather than sentence casing.
88
209
  if (/^\p{Ll}[\p{Ll}\p{N}]*\p{Lu}/u.test(leadingWord)) {
89
210
  return text;
90
211
  }
package/comments.json CHANGED
@@ -6,18 +6,21 @@
6
6
  }
7
7
  ],
8
8
  "rules": {
9
- "comments/block-comments": "error",
10
9
  "comments/class-documentation": "error",
11
10
  "comments/configured-api-calls": "error",
11
+ "comments/formatting": "error",
12
12
  "comments/function-documentation": "error",
13
- "comments/jsdoc-tag-formatting": "error",
14
- "comments/line-comments": "error",
15
- "comments/max-line-length": "error",
16
- "comments/placement": "error",
17
- "comments/sentence-punctuation": "error",
18
13
  "comments/variable-declarations": "error",
19
14
  "comments/vue-component-documentation": "error",
20
15
  "comments/vue-emit-documentation": "error",
21
16
  "comments/vue-prop-documentation": "error"
22
- }
17
+ },
18
+ "overrides": [
19
+ {
20
+ "files": ["**/*.test.*", "**/*.spec.*", "**/test/**"],
21
+ "rules": {
22
+ "comments/variable-declarations": ["error", { "rootOnly": true }]
23
+ }
24
+ }
25
+ ]
23
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/lint-config",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Shared oxlint configuration for Lewis Howles projects",
5
5
  "keywords": [
6
6
  "config",
@@ -40,7 +40,8 @@
40
40
  "lint": "vp check",
41
41
  "lint:fix": "vp check --fix",
42
42
  "prepare": "vp config --no-agent",
43
- "publint": "publint"
43
+ "publint": "publint",
44
+ "test:unit": "node --test test/comments/*.test.js"
44
45
  },
45
46
  "devDependencies": {
46
47
  "publint": "^0.3.22",