@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 +5 -2
- package/js/eslint/index.js +7 -2
- package/js/eslint/plugin.js +2 -0
- package/js/eslint/rules/align-doc-tags.js +125 -0
- package/package.json +1 -1
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
|
|
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
|
|
package/js/eslint/index.js
CHANGED
|
@@ -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
|
|
58
|
-
// signature, never
|
|
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
|
{
|
package/js/eslint/plugin.js
CHANGED
|
@@ -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.
|
|
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>",
|