@sinemacula/coding-standards 1.10.0 → 1.12.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 CHANGED
@@ -242,10 +242,12 @@ native directive - `// phpcs:ignore <code>` for a sniff, `@phpstan-ignore <ident
242
242
  | `SineMacula.Classes.RequireReadonlyPublicProperty` | Public properties (declared or promoted) must be `readonly`. |
243
243
  | `SineMacula.Commenting.CommentLineLength` | Standalone comment lines must not exceed 80 chars (FQCN/URL exempt). |
244
244
  | `SineMacula.Commenting.ConsistentEnumCaseComments` | Enum case docs are all-or-nothing within an enum. |
245
+ | `SineMacula.Commenting.MultilineMethodComment` | A method's doc comment must span multiple lines. |
245
246
  | `SineMacula.Commenting.RequireConstantComment` | Every class/interface/enum/trait constant needs a doc comment. |
246
247
  | `SineMacula.Commenting.RequireCopyrightTag` | Class/interface/enum/trait docblocks must carry an `@copyright` tag. |
247
248
  | `SineMacula.Commenting.RequireNonPromotedParameterComment` | Plain params mixed with promoted ones need a comment. |
248
249
  | `SineMacula.Commenting.RequirePromotedPropertyComment` | Every constructor-promoted property needs a doc comment. |
250
+ | `SineMacula.Commenting.SingleLineMemberComment` | A property, constant or enum-case doc comment sits on one line. |
249
251
  | `SineMacula.Exceptions.DisallowBaseException` | No throwing the base `\Exception`; throw a domain exception. |
250
252
  | `SineMacula.Exceptions.RequireEmptyCatchComment` | An empty catch block must comment its intentional swallow. |
251
253
  | `SineMacula.Functions.RequireSensitiveParameter` | Secret-named params need `#[\SensitiveParameter]`. |
@@ -282,15 +284,26 @@ type-checked layer.
282
284
  | `@sinemacula/max-methods-per-class` | A single class may declare at most 20 methods; test code exempt. |
283
285
  | `@sinemacula/no-base-error` | Throw a domain-specific `Error` subclass, never the base `Error`; test code exempt. |
284
286
  | `@sinemacula/require-copyright` | Every file must carry a documentation comment with `@copyright` and `@author`. |
287
+ | `@sinemacula/align-doc-tags` | `@author` and `@copyright` values line up at a single column; autofixable. |
288
+ | `@sinemacula/pad-block-start` | An interface, class or enum body opens with a blank line; autofixable. |
289
+ | `@sinemacula/single-line-property-doc` | A data member's documentation comment sits on one line; autofixable. |
290
+ | `@sinemacula/multiline-function-doc` | A method's documentation comment spans multiple lines; autofixable. |
285
291
 
286
292
  `boolean-method-name` takes `additionalPrefixes`, `additionalPredicates` and `additionalCommandVerbs` (string arrays)
287
293
  to widen the accepted vocabulary from a consumer config. `max-methods-per-class` takes `max`, `no-base-error` takes
288
- `allow`, and `require-copyright` takes `tags` to adjust their defaults.
294
+ `allow`, and `require-copyright` takes `tags` to adjust their defaults. `align-doc-tags` takes `tags` and `column`,
295
+ the column counting from the `@`, so the default of 14 gives `@author` six spaces and `@copyright` three.
296
+ `pad-block-start` governs the opening brace only, leaving the closing brace against the final member. Together
297
+ `single-line-property-doc` and `multiline-function-doc` set a member's comment shape by its kind: data members
298
+ (interface property signatures, enum members and data class fields) take one line, while methods, interface method
299
+ signatures and class fields holding a function take several. A data comment is never required, only held to one line
300
+ where present; a free function keeps the freedom of either shape.
289
301
 
290
302
  The base layer also switches on a set of built-in rules: `@typescript-eslint/no-explicit-any`, `max-lines-per-function`
291
303
  (50 lines, test code exempt) and `max-depth` (4), plus `eslint-plugin-jsdoc` rules that require a documentation comment
292
- on every declared function, method and class and forbid types in `@param`/`@returns` (types belong in the signature).
293
- The type-checked layer adds `@typescript-eslint/explicit-module-boundary-types` and
304
+ on every declared function, method, class, interface member and class field, forbid types in `@param`/`@returns` (the
305
+ tags themselves are welcome, types belong in the signature) and keep a blank line above every documentation block,
306
+ single-line blocks included. The type-checked layer adds `@typescript-eslint/explicit-module-boundary-types` and
294
307
  `@typescript-eslint/only-throw-error`.
295
308
 
296
309
  ## Requirements
@@ -49,13 +49,22 @@ export default [
49
49
  '@sinemacula/max-methods-per-class': 'error',
50
50
  '@sinemacula/no-base-error': 'error',
51
51
  '@sinemacula/require-copyright': 'error',
52
+ '@sinemacula/align-doc-tags': 'error',
53
+ '@sinemacula/pad-block-start': 'error',
54
+ '@sinemacula/single-line-property-doc': 'error',
55
+ '@sinemacula/multiline-function-doc': 'error',
52
56
 
53
57
  'max-lines-per-function': ['error', { max: 50, skipComments: true, skipBlankLines: true, IIFEs: true }],
54
58
  'max-depth': ['error', 4],
55
59
 
56
- // Every declared function, method, class and assigned arrow carries
57
- // a documentation comment describing intent; types live in the
58
- // signature, never in the comment (no @param/@returns type tags).
60
+ // Every declared function, method, class, assigned arrow and
61
+ // property carries a documentation comment describing intent, so a
62
+ // reader meets each member's purpose before its type. Interface
63
+ // members and class fields are held to the same bar as methods.
64
+ // Types live in the signature, so a tag never annotates one: the
65
+ // @param and @returns tags themselves are welcome, only their type
66
+ // forms are not. Each block stands off from the code above it,
67
+ // single-line blocks included.
59
68
  'jsdoc/require-jsdoc': ['error', {
60
69
  require: {
61
70
  ClassDeclaration: true,
@@ -66,13 +75,21 @@ export default [
66
75
  contexts: [
67
76
  'VariableDeclarator > ArrowFunctionExpression',
68
77
  'VariableDeclarator > FunctionExpression',
78
+ 'TSPropertySignature',
79
+ 'TSMethodSignature',
80
+ 'PropertyDefinition',
69
81
  ],
70
82
  checkConstructors: false,
83
+ // Report only; the autofix inserts an empty stub that then
84
+ // fails require-description, so a blind fix would scatter
85
+ // hollow comments rather than resolve the finding.
86
+ enableFixer: false,
71
87
  }],
72
88
  'jsdoc/require-description': 'error',
73
89
  'jsdoc/no-types': 'error',
74
90
  'jsdoc/require-param-description': 'error',
75
91
  'jsdoc/require-returns-description': 'error',
92
+ 'jsdoc/lines-before-block': ['error', { lines: 1, ignoreSingleLines: false }],
76
93
  },
77
94
  },
78
95
  {
@@ -1,10 +1,14 @@
1
+ import alignDocTags from './rules/align-doc-tags.js';
1
2
  import booleanMethodName from './rules/boolean-method-name.js';
2
3
  import maxMethodsPerClass from './rules/max-methods-per-class.js';
4
+ import multilineFunctionDoc from './rules/multiline-function-doc.js';
3
5
  import noBaseError from './rules/no-base-error.js';
4
6
  import noInterfacePrefix from './rules/no-interface-prefix.js';
5
7
  import noMutableStatic from './rules/no-mutable-static.js';
8
+ import padBlockStart from './rules/pad-block-start.js';
6
9
  import requireCopyright from './rules/require-copyright.js';
7
10
  import requireReadonlyPublicProperty from './rules/require-readonly-public-property.js';
11
+ import singleLinePropertyDoc from './rules/single-line-property-doc.js';
8
12
  import validEnumMemberName from './rules/valid-enum-member-name.js';
9
13
 
10
14
  /**
@@ -30,5 +34,9 @@ export default {
30
34
  'max-methods-per-class': maxMethodsPerClass,
31
35
  'no-base-error': noBaseError,
32
36
  'require-copyright': requireCopyright,
37
+ 'align-doc-tags': alignDocTags,
38
+ 'pad-block-start': padBlockStart,
39
+ 'single-line-property-doc': singleLinePropertyDoc,
40
+ 'multiline-function-doc': multilineFunctionDoc,
33
41
  },
34
42
  };
@@ -0,0 +1,125 @@
1
+ import { createRule } from './lib.js';
2
+
3
+ const DEFAULT_TAGS = ['author', 'copyright'];
4
+ const DEFAULT_COLUMN = 14;
5
+
6
+ /** A documentation line opening a tag that carries a value. */
7
+ const TAG_LINE = /^(\s*\*\s*)@([A-Za-z][\w-]*)([^\S\n]+)(?=\S)/;
8
+
9
+ /**
10
+ * The spaces that place a tag's value at the target column, or null when the
11
+ * tag is too long to reach it.
12
+ */
13
+ function padding(tag, column) {
14
+ const spaces = column - tag.length - 2;
15
+
16
+ return spaces > 0 ? ' '.repeat(spaces) : null;
17
+ }
18
+
19
+ /**
20
+ * Align the values of the documentation tags a file's header declares,
21
+ * `@author` and `@copyright` by default.
22
+ *
23
+ * The tags a file must carry read as a block, so their values line up at a
24
+ * single column rather than sitting at whatever offset each tag's own length
25
+ * happens to produce. The column counts from the `@`, which at the default of
26
+ * 14 gives `@author` six spaces and `@copyright` three.
27
+ *
28
+ * Only the run of whitespace between a listed tag and its value is considered,
29
+ * and only on the line the tag opens; a wrapped value's continuation lines, a
30
+ * tag with no value and every unlisted tag are left alone. A tag longer than
31
+ * the column can accommodate is skipped rather than reported, as no spacing
32
+ * would satisfy the requirement. Presence of the tags is a separate concern,
33
+ * enforced by `require-copyright`.
34
+ *
35
+ * @author Ben Carey <bdmc@sinemacula.co.uk>
36
+ * @copyright 2026 Sine Macula Limited
37
+ */
38
+ export default createRule({
39
+ name: 'align-doc-tags',
40
+ meta: {
41
+ type: 'layout',
42
+ fixable: 'whitespace',
43
+ docs: {
44
+ description: 'Align the values of the documentation tags a file header declares.',
45
+ },
46
+ schema: [
47
+ {
48
+ type: 'object',
49
+ properties: {
50
+ tags: {
51
+ type: 'array',
52
+ items: { type: 'string' },
53
+ },
54
+ column: {
55
+ type: 'integer',
56
+ minimum: 1,
57
+ },
58
+ },
59
+ additionalProperties: false,
60
+ },
61
+ ],
62
+ messages: {
63
+ misaligned: 'The @{{ tag }} value must start at column {{ column }}.',
64
+ },
65
+ },
66
+ defaultOptions: [{ tags: DEFAULT_TAGS, column: DEFAULT_COLUMN }],
67
+ create(context, [options]) {
68
+ const { sourceCode } = context;
69
+ const tags = new Set((options.tags ?? DEFAULT_TAGS).map(tag => tag.toLowerCase()));
70
+ const column = options.column ?? DEFAULT_COLUMN;
71
+
72
+ /** Report and fix the spacing a single tag line carries. */
73
+ function inspect(line, start) {
74
+ const match = TAG_LINE.exec(line);
75
+
76
+ if (!match) {
77
+ return;
78
+ }
79
+
80
+ const [, prefix, tag, spacing] = match;
81
+
82
+ if (!tags.has(tag.toLowerCase())) {
83
+ return;
84
+ }
85
+
86
+ const desired = padding(tag, column);
87
+
88
+ if (desired === null || spacing === desired) {
89
+ return;
90
+ }
91
+
92
+ const from = start + prefix.length + 1 + tag.length;
93
+ const to = from + spacing.length;
94
+
95
+ context.report({
96
+ loc: {
97
+ start: sourceCode.getLocFromIndex(from),
98
+ end: sourceCode.getLocFromIndex(to),
99
+ },
100
+ messageId: 'misaligned',
101
+ data: { tag, column },
102
+ fix: fixer => fixer.replaceTextRange([from, to], desired),
103
+ });
104
+ }
105
+
106
+ return {
107
+ Program() {
108
+ for (const comment of sourceCode.getAllComments()) {
109
+ if (comment.type !== 'Block' || !comment.value.startsWith('*')) {
110
+ continue;
111
+ }
112
+
113
+ // The comment's value begins after the opening `/*`, so an
114
+ // offset within it maps onto the source two characters in.
115
+ let offset = comment.range[0] + 2;
116
+
117
+ for (const line of comment.value.split('\n')) {
118
+ inspect(line, offset);
119
+ offset += line.length + 1;
120
+ }
121
+ }
122
+ },
123
+ };
124
+ },
125
+ });
@@ -20,8 +20,12 @@ const COMMAND_VERBS = new Set([
20
20
  'emit', 'apply', 'guard', 'validate', 'verify', 'authorize', 'ensure',
21
21
  'assert', 'register', 'boot', 'build', 'make', 'resolve', 'render',
22
22
  'compute', 'calculate', 'expose', 'parse', 'format', 'transform', 'toggle',
23
+ 'submit', 'confirm', 'copy', 'attempt',
23
24
  ]);
24
25
 
26
+ /** Method names that are a primitive type keyword: typed accessors, not predicates. */
27
+ const TYPE_ACCESSOR_NAMES = new Set(['boolean']);
28
+
25
29
  /** Matches @imperative only at a docblock tag position, never inside prose. */
26
30
  const IMPERATIVE_TAG = /^[ \t*]*@imperative(?![-\w])/im;
27
31
 
@@ -50,6 +54,11 @@ function isCommandVerb(name, verbs) {
50
54
  return verbs.has(firstWord(name));
51
55
  }
52
56
 
57
+ /** Whether the name is an on* event-handler callback (onError, onUnauthorized). */
58
+ function isEventHandler(name) {
59
+ return /^on[A-Z]/.test(name);
60
+ }
61
+
53
62
  /**
54
63
  * Whether a return type resolves to boolean, ignoring a nullable `?bool`-style
55
64
  * null/undefined/void tail so an optional boolean still counts. Promise
@@ -143,6 +152,8 @@ function inspect(state, nameNode, name, fnNode, docHost) {
143
152
  name.startsWith('__')
144
153
  || isPredicate(name, state.prefixes, state.predicates)
145
154
  || isCommandVerb(name, state.commandVerbs)
155
+ || isEventHandler(name)
156
+ || TYPE_ACCESSOR_NAMES.has(name)
146
157
  || hasImperativeTag(sourceCode, docHost, nameNode)
147
158
  ) {
148
159
  return;
@@ -266,8 +277,10 @@ function buildListeners(state) {
266
277
  * succeeded, failed, expired). An imperative command verb (execute, persist,
267
278
  * guard, ...) that returns a result bool is exempt via COMMAND_VERBS. A member
268
279
  * may also opt out with an
269
- * @imperative docblock tag. Accessors, the constructor, computed names, magic
270
- * names and type-predicate guards (x is T) are exempt. The return type is
280
+ * @imperative docblock tag. An on* event-handler callback (onError) and a
281
+ * method named after a primitive type (a typed accessor such as boolean())
282
+ * are exempt, as are accessors, the constructor, computed names, magic names
283
+ * and type-predicate guards (x is T). The return type is
271
284
  * resolved from type information - inferred booleans and awaited
272
285
  * Promise<boolean> included - so the rule degrades to a no-op when no type
273
286
  * information is available. The accepted vocabulary can be widened per consumer
@@ -0,0 +1,103 @@
1
+ import { createRule } from './lib.js';
2
+
3
+ /** Values that make a class field read as behaviour rather than data. */
4
+ const FUNCTION_VALUES = new Set(['ArrowFunctionExpression', 'FunctionExpression']);
5
+
6
+ /**
7
+ * Whether the node declares a method or a method-like member: a class method,
8
+ * an interface method signature, or a class field that holds a function.
9
+ */
10
+ function isMethodMember(node) {
11
+ switch (node.type) {
12
+ case 'MethodDefinition':
13
+ case 'TSAbstractMethodDefinition':
14
+ case 'TSMethodSignature':
15
+ return true;
16
+ case 'PropertyDefinition':
17
+ return Boolean(node.value) && FUNCTION_VALUES.has(node.value.type);
18
+ default:
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /** The single line of prose a one-line documentation block carries. */
24
+ function singleLineContent(value) {
25
+ return value.replace(/^\*/, '').trim();
26
+ }
27
+
28
+ /**
29
+ * Require a method's documentation comment to span multiple lines.
30
+ *
31
+ * A method earns a fuller account than a data field: what it does, and in time
32
+ * its parameters and what it returns. Holding its comment open across lines
33
+ * keeps that room ready and sets behaviour apart from data at a glance, the
34
+ * mirror of the single-line form data properties take. Class methods, interface
35
+ * method signatures and class fields that hold a function are governed; a data
36
+ * property keeps its one line, and a free function is left to the author, since
37
+ * a short helper reads well on a single line.
38
+ *
39
+ * A block already spanning lines passes. A one-line block is unfolded onto
40
+ * three, its prose moved to the middle line; an empty one is left for the
41
+ * description rule to report, since there is nothing to unfold.
42
+ *
43
+ * @author Ben Carey <bdmc@sinemacula.co.uk>
44
+ * @copyright 2026 Sine Macula Limited
45
+ */
46
+ export default createRule({
47
+ name: 'multiline-function-doc',
48
+ meta: {
49
+ type: 'layout',
50
+ fixable: 'whitespace',
51
+ docs: {
52
+ description: 'Require a method documentation comment to span multiple lines.',
53
+ },
54
+ schema: [],
55
+ messages: {
56
+ singleLine: 'A method documentation comment must span multiple lines.',
57
+ },
58
+ },
59
+ defaultOptions: [],
60
+ create(context) {
61
+ const { sourceCode } = context;
62
+
63
+ /** Unfold a method's one-line documentation block onto three lines. */
64
+ function inspect(node) {
65
+ if (!isMethodMember(node)) {
66
+ return;
67
+ }
68
+
69
+ const doc = sourceCode.getCommentsBefore(node).at(-1);
70
+
71
+ if (!doc || doc.type !== 'Block' || !doc.value.startsWith('*')) {
72
+ return;
73
+ }
74
+
75
+ if (doc.loc.start.line !== doc.loc.end.line) {
76
+ return;
77
+ }
78
+
79
+ const content = singleLineContent(doc.value);
80
+
81
+ // An empty block is the description rule's to report; there is no
82
+ // prose to move onto the middle line.
83
+ if (content.length === 0) {
84
+ return;
85
+ }
86
+
87
+ const pad = ' '.repeat(doc.loc.start.column);
88
+
89
+ context.report({
90
+ node: doc,
91
+ messageId: 'singleLine',
92
+ fix: fixer => fixer.replaceText(doc, `/**\n${pad} * ${content}\n${pad} */`),
93
+ });
94
+ }
95
+
96
+ return {
97
+ MethodDefinition: inspect,
98
+ TSAbstractMethodDefinition: inspect,
99
+ TSMethodSignature: inspect,
100
+ PropertyDefinition: inspect,
101
+ };
102
+ },
103
+ });
@@ -0,0 +1,85 @@
1
+ import { createRule } from './lib.js';
2
+
3
+ /** The member-carrying bodies whose first line is held clear of the brace. */
4
+ const BODY_TYPES = new Set(['TSInterfaceBody', 'ClassBody', 'TSEnumBody']);
5
+
6
+ /**
7
+ * Require a blank line after the opening brace of an interface, class or enum
8
+ * body.
9
+ *
10
+ * The first member then stands off from the declaration the way every later
11
+ * member stands off from the one above it, so a body reads as an evenly spaced
12
+ * list rather than crowding its opening line. Only the opening brace is
13
+ * governed; the closing brace is left to sit against the final member.
14
+ *
15
+ * An empty body and a body written entirely on one line carry no member to
16
+ * separate and are left alone. Where the first member opens with a
17
+ * documentation comment the blank line falls above the comment, so the comment
18
+ * stays attached to what it documents.
19
+ *
20
+ * @author Ben Carey <bdmc@sinemacula.co.uk>
21
+ * @copyright 2026 Sine Macula Limited
22
+ */
23
+ export default createRule({
24
+ name: 'pad-block-start',
25
+ meta: {
26
+ type: 'layout',
27
+ fixable: 'whitespace',
28
+ docs: {
29
+ description: 'Require a blank line after the opening brace of an interface, class or enum body.',
30
+ },
31
+ schema: [],
32
+ messages: {
33
+ missing: 'A body must begin with a blank line after its opening brace.',
34
+ },
35
+ },
36
+ defaultOptions: [],
37
+ create(context) {
38
+ const { sourceCode } = context;
39
+
40
+ /** Hold the first member of a body clear of the opening brace. */
41
+ function inspect(node) {
42
+ const brace = sourceCode.getFirstToken(node);
43
+ const close = sourceCode.getLastToken(node);
44
+
45
+ // A one-line body has no member to separate from the brace.
46
+ if (!brace || !close || brace.loc.end.line === close.loc.start.line) {
47
+ return;
48
+ }
49
+
50
+ const first = sourceCode.getTokenAfter(brace, { includeComments: true });
51
+
52
+ // An empty body carries nothing to stand off from the brace.
53
+ if (!first || first === close) {
54
+ return;
55
+ }
56
+
57
+ const blanks = first.loc.start.line - brace.loc.end.line - 1;
58
+
59
+ if (blanks === 1) {
60
+ return;
61
+ }
62
+
63
+ const onOwnLine = first.loc.start.line > brace.loc.end.line;
64
+
65
+ context.report({
66
+ loc: brace.loc,
67
+ messageId: 'missing',
68
+ fix: onOwnLine
69
+ ? fixer => fixer.replaceTextRange(
70
+ [brace.range[1], first.range[0]],
71
+ `\n\n${' '.repeat(first.loc.start.column)}`,
72
+ )
73
+ : null,
74
+ });
75
+ }
76
+
77
+ const visitor = {};
78
+
79
+ for (const type of BODY_TYPES) {
80
+ visitor[type] = inspect;
81
+ }
82
+
83
+ return visitor;
84
+ },
85
+ });
@@ -0,0 +1,107 @@
1
+ import { createRule } from './lib.js';
2
+
3
+ /** Class-field values that read as behaviour rather than data. */
4
+ const FUNCTION_VALUES = new Set(['ArrowFunctionExpression', 'FunctionExpression']);
5
+
6
+ /**
7
+ * Whether the node is a data member: an interface property signature, an enum
8
+ * member, or a class field that does not hold a function.
9
+ */
10
+ function isDataProperty(node) {
11
+ if (node.type === 'TSPropertySignature' || node.type === 'TSEnumMember') {
12
+ return true;
13
+ }
14
+
15
+ return node.type === 'PropertyDefinition'
16
+ && !(node.value && FUNCTION_VALUES.has(node.value.type));
17
+ }
18
+
19
+ /**
20
+ * The text lines a documentation block carries, each stripped of its leading
21
+ * margin and asterisk, with blank lines dropped.
22
+ */
23
+ function contentLines(value) {
24
+ return value
25
+ .split('\n')
26
+ .map(line => line.replace(/^\s*\*?[^\S\n]?/, '').trimEnd())
27
+ .filter(line => line.length > 0);
28
+ }
29
+
30
+ /**
31
+ * Require a data member's documentation comment to sit on a single line.
32
+ *
33
+ * A field's description is a single phrase, so it belongs on one line however
34
+ * long it runs; nothing wraps a comment to a width, and breaking it by hand
35
+ * only scatters the phrase down the file. Interface property signatures, enum
36
+ * members and class fields that hold data are governed; a class field holding a
37
+ * function reads as behaviour, may document parameters and is left to span as
38
+ * many lines as it needs, as are methods and every free function. A comment is
39
+ * never required here, only held to one line where it is present, so enum
40
+ * members stay documented at the author's discretion.
41
+ *
42
+ * A block already on one line passes. A multi-line block collapses to one, its
43
+ * lines joined by single spaces, provided it carries prose alone: a block
44
+ * bearing a tag such as `@deprecated` keeps its shape and is reported without a
45
+ * fix, since folding a tag into a running line would change its meaning.
46
+ *
47
+ * @author Ben Carey <bdmc@sinemacula.co.uk>
48
+ * @copyright 2026 Sine Macula Limited
49
+ */
50
+ export default createRule({
51
+ name: 'single-line-property-doc',
52
+ meta: {
53
+ type: 'layout',
54
+ fixable: 'whitespace',
55
+ docs: {
56
+ description: 'Require a data member documentation comment to sit on a single line.',
57
+ },
58
+ schema: [],
59
+ messages: {
60
+ multiline: 'A data member documentation comment must sit on a single line.',
61
+ },
62
+ },
63
+ defaultOptions: [],
64
+ create(context) {
65
+ const { sourceCode } = context;
66
+
67
+ /** Fold a data property's leading documentation block onto one line. */
68
+ function inspect(node) {
69
+ if (!isDataProperty(node)) {
70
+ return;
71
+ }
72
+
73
+ const comments = sourceCode.getCommentsBefore(node);
74
+ const doc = comments.at(-1);
75
+
76
+ if (!doc || doc.type !== 'Block' || !doc.value.startsWith('*')) {
77
+ return;
78
+ }
79
+
80
+ if (doc.loc.start.line === doc.loc.end.line) {
81
+ return;
82
+ }
83
+
84
+ const lines = contentLines(doc.value);
85
+
86
+ // An empty block is the description rule's to report; there is no
87
+ // prose to fold onto one line.
88
+ if (lines.length === 0) {
89
+ return;
90
+ }
91
+
92
+ const hasTag = lines.some(line => line.startsWith('@'));
93
+
94
+ context.report({
95
+ node: doc,
96
+ messageId: 'multiline',
97
+ fix: hasTag ? null : fixer => fixer.replaceText(doc, `/** ${lines.join(' ')} */`),
98
+ });
99
+ }
100
+
101
+ return {
102
+ TSPropertySignature: inspect,
103
+ TSEnumMember: inspect,
104
+ PropertyDefinition: inspect,
105
+ };
106
+ },
107
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sinemacula/coding-standards",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Centralized coding standards, static analysis configurations, and code quality tooling for all Sine Macula repositories.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Ben Carey <bdmc@sinemacula.co.uk>",