@sinemacula/coding-standards 1.10.0 → 1.11.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
@@ -282,14 +282,17 @@ type-checked layer.
282
282
  | `@sinemacula/max-methods-per-class` | A single class may declare at most 20 methods; test code exempt. |
283
283
  | `@sinemacula/no-base-error` | Throw a domain-specific `Error` subclass, never the base `Error`; test code exempt. |
284
284
  | `@sinemacula/require-copyright` | Every file must carry a documentation comment with `@copyright` and `@author`. |
285
+ | `@sinemacula/align-doc-tags` | `@author` and `@copyright` values line up at a single column; autofixable. |
285
286
 
286
287
  `boolean-method-name` takes `additionalPrefixes`, `additionalPredicates` and `additionalCommandVerbs` (string arrays)
287
288
  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.
289
+ `allow`, and `require-copyright` takes `tags` to adjust their defaults. `align-doc-tags` takes `tags` and `column`,
290
+ the column counting from the `@`, so the default of 14 gives `@author` six spaces and `@copyright` three.
289
291
 
290
292
  The base layer also switches on a set of built-in rules: `@typescript-eslint/no-explicit-any`, `max-lines-per-function`
291
293
  (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).
294
+ on every declared function, method and class, forbid types in `@param`/`@returns` (the tags themselves are welcome,
295
+ types belong in the signature) and keep a blank line above every documentation block, single-line blocks included.
293
296
  The type-checked layer adds `@typescript-eslint/explicit-module-boundary-types` and
294
297
  `@typescript-eslint/only-throw-error`.
295
298
 
@@ -49,13 +49,17 @@ 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',
52
53
 
53
54
  'max-lines-per-function': ['error', { max: 50, skipComments: true, skipBlankLines: true, IIFEs: true }],
54
55
  'max-depth': ['error', 4],
55
56
 
56
57
  // 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).
58
+ // a documentation comment describing intent. Types live in the
59
+ // signature, so a tag never annotates one: the @param and @returns
60
+ // tags themselves are welcome, only their type forms are not. Each
61
+ // block stands off from the code above it, single-line blocks
62
+ // included.
59
63
  'jsdoc/require-jsdoc': ['error', {
60
64
  require: {
61
65
  ClassDeclaration: true,
@@ -73,6 +77,7 @@ export default [
73
77
  'jsdoc/no-types': 'error',
74
78
  'jsdoc/require-param-description': 'error',
75
79
  'jsdoc/require-returns-description': 'error',
80
+ 'jsdoc/lines-before-block': ['error', { lines: 1, ignoreSingleLines: false }],
76
81
  },
77
82
  },
78
83
  {
@@ -1,3 +1,4 @@
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';
3
4
  import noBaseError from './rules/no-base-error.js';
@@ -30,5 +31,6 @@ export default {
30
31
  'max-methods-per-class': maxMethodsPerClass,
31
32
  'no-base-error': noBaseError,
32
33
  'require-copyright': requireCopyright,
34
+ 'align-doc-tags': alignDocTags,
33
35
  },
34
36
  };
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sinemacula/coding-standards",
3
- "version": "1.10.0",
3
+ "version": "1.11.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>",