@sinemacula/coding-standards 1.22.0 → 1.23.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 +31 -12
- package/js/eslint/index.js +10 -1
- package/js/eslint/plugin.js +2 -0
- package/js/eslint/rules/lib.js +24 -0
- package/js/eslint/rules/require-copyright.js +5 -15
- package/js/eslint/rules/require-file-description.js +97 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -417,19 +417,30 @@ type-checked layer. Every rule is scoped to `.ts`/`.js`; `comment-line-wrap` alo
|
|
|
417
417
|
| `@sinemacula/max-methods-per-class` | A single class may declare at most 20 methods; test code exempt. |
|
|
418
418
|
| `@sinemacula/no-base-error` | Throw a domain-specific `Error` subclass, never the base `Error`; test code exempt. |
|
|
419
419
|
| `@sinemacula/require-copyright` | Every file must carry a documentation comment with `@copyright` and `@author`. |
|
|
420
|
+
| `@sinemacula/require-file-description` | The same comment must open with a prose summary above those tags. |
|
|
420
421
|
| `@sinemacula/align-doc-tags` | `@author` and `@copyright` values line up at a single column; autofixable. |
|
|
421
422
|
| `@sinemacula/single-line-property-doc` | A data member's documentation comment sits on one line; autofixable. |
|
|
422
423
|
| `@sinemacula/multiline-function-doc` | A method's documentation comment spans multiple lines; autofixable. |
|
|
423
424
|
| `@sinemacula/comment-line-wrap` | Standalone comment prose wrapped to 80 chars, YAML included; premature wraps too. |
|
|
424
425
|
|
|
425
|
-
`boolean-method-name` takes `additionalPrefixes`, `additionalPredicates` and `additionalCommandVerbs` (string arrays)
|
|
426
|
-
|
|
427
|
-
`allow`, and `require-copyright`
|
|
428
|
-
column counting from the `@`, so the default of 14 gives `@author` six
|
|
429
|
-
`single-line-property-doc` and `multiline-function-doc` set a member's comment
|
|
430
|
-
(interface property signatures, enum members and data class fields) take one line, while
|
|
431
|
-
signatures and class fields holding a function take several. A data comment is never required,
|
|
432
|
-
where present; a free function keeps the freedom of either shape.
|
|
426
|
+
`boolean-method-name` takes `additionalPrefixes`, `additionalPredicates` and `additionalCommandVerbs` (string arrays) to
|
|
427
|
+
widen the accepted vocabulary from a consumer config. `max-methods-per-class` takes `max`, `no-base-error` takes
|
|
428
|
+
`allow`, and `require-copyright` and `require-file-description` each take `tags` to adjust their defaults.
|
|
429
|
+
`align-doc-tags` takes `tags` and `column`, the column counting from the `@`, so the default of 14 gives `@author` six
|
|
430
|
+
spaces and `@copyright` three. Together `single-line-property-doc` and `multiline-function-doc` set a member's comment
|
|
431
|
+
shape by its kind: data members (interface property signatures, enum members and data class fields) take one line, while
|
|
432
|
+
methods, interface method signatures and class fields holding a function take several. A data comment is never required,
|
|
433
|
+
only held to one line where present; a free function keeps the freedom of either shape.
|
|
434
|
+
|
|
435
|
+
`require-copyright` and `require-file-description` divide the file header between them: the first asks that a single
|
|
436
|
+
block comment carry `@copyright` and `@author`, the second that the same block open with a prose summary above them,
|
|
437
|
+
which is the shape the convention has always described and the half nothing enforced. Both find that block by the
|
|
438
|
+
tags it carries rather than by its position, so a module documented at its export below the imports is read as the
|
|
439
|
+
header exactly as a comment at the top of the file is; a project that narrows one rule's `tags` should narrow the
|
|
440
|
+
other's to match. Only the block's first non-blank line is read, and only for whether it is prose rather than a tag,
|
|
441
|
+
which puts the summary above the tags and stops a wrapped tag value's continuation line passing as one. What the
|
|
442
|
+
summary says is the author's business. A file carrying no such block at all is left to `require-copyright`, so one
|
|
443
|
+
missing header is never faulted twice.
|
|
433
444
|
|
|
434
445
|
`comment-line-wrap` takes `maxLength` (default 80) and is the syntax-only counterpart of the PHP
|
|
435
446
|
`SineMacula.Commenting.CommentLineLength` sniff. It fills standalone `//` and `#` runs and multi-line docblock prose
|
|
@@ -450,10 +461,18 @@ than comment, so a shell comment inside a `run: |` step is never seen, and a com
|
|
|
450
461
|
|
|
451
462
|
The base layer also switches on a set of built-in rules: `@typescript-eslint/no-explicit-any`, `curly` (a brace on every
|
|
452
463
|
control statement, as PSR-12 already requires on the PHP side), `max-lines-per-function` (50 lines, test code exempt)
|
|
453
|
-
and `max-depth` (4), plus `eslint-plugin-jsdoc` rules that require a documentation comment on every
|
|
454
|
-
method, class, interface member and class field, require a description on every `@param` and
|
|
455
|
-
blank line above every documentation block, single-line blocks included. The type-checked layer
|
|
456
|
-
`@typescript-eslint/explicit-module-boundary-types` and `@typescript-eslint/only-throw-error`.
|
|
464
|
+
and `max-depth` (4, test code exempt), plus `eslint-plugin-jsdoc` rules that require a documentation comment on every
|
|
465
|
+
declared function, method, class, interface member and class field, require a description on every `@param` and
|
|
466
|
+
`@returns`, and keep a blank line above every documentation block, single-line blocks included. The type-checked layer
|
|
467
|
+
adds `@typescript-eslint/explicit-module-boundary-types` and `@typescript-eslint/only-throw-error`.
|
|
468
|
+
|
|
469
|
+
Test files - `*.test.*`, `*.spec.*` and anything under `__tests__/`, `tests/` or `test-support/` - are exempt from
|
|
470
|
+
`max-lines-per-function` and `max-depth` alone. A spec body is a long, deeply nested account of one scenario, and
|
|
471
|
+
splitting it to satisfy a metric hides the scenario rather than simplifying it. They are not exempt from documentation:
|
|
472
|
+
`jsdoc/require-jsdoc` reaches declared functions, assigned arrows and classes, which in a spec file are its helpers,
|
|
473
|
+
factories and builders - the code a reader has to understand before the assertions mean anything. A test case is an
|
|
474
|
+
arrow passed as a call argument, which none of the rule's contexts match, so `it('...', () => {})` is never asked to
|
|
475
|
+
carry a block; a spec of test cases alone carries no documentation burden at all.
|
|
457
476
|
|
|
458
477
|
`jsdoc/no-types`, which forbids a type in `@param`/`@returns`, runs over `.ts`/`.tsx`/`.mts`/`.cts` alone, alongside
|
|
459
478
|
`@typescript-eslint/no-explicit-any`. A TypeScript signature already records the type, so the tag would only repeat it
|
package/js/eslint/index.js
CHANGED
|
@@ -63,6 +63,7 @@ export default [
|
|
|
63
63
|
'@sinemacula/max-methods-per-class': 'error',
|
|
64
64
|
'@sinemacula/no-base-error': 'error',
|
|
65
65
|
'@sinemacula/require-copyright': 'error',
|
|
66
|
+
'@sinemacula/require-file-description': 'error',
|
|
66
67
|
'@sinemacula/align-doc-tags': 'error',
|
|
67
68
|
'@sinemacula/single-line-property-doc': 'error',
|
|
68
69
|
'@sinemacula/multiline-function-doc': 'error',
|
|
@@ -125,11 +126,19 @@ export default [
|
|
|
125
126
|
},
|
|
126
127
|
},
|
|
127
128
|
{
|
|
129
|
+
// Test code keeps the length and depth exemptions and nothing else. A
|
|
130
|
+
// spec body is a long, deeply nested account of one scenario, and
|
|
131
|
+
// splitting it to satisfy a metric hides the scenario rather than
|
|
132
|
+
// simplifying it. Documentation is a different matter: require-jsdoc's
|
|
133
|
+
// contexts reach declared functions, assigned arrows and classes, which
|
|
134
|
+
// in a spec file are its helpers, factories and builders - the code a
|
|
135
|
+
// reader has to understand before the assertions mean anything. A test
|
|
136
|
+
// case itself is an arrow passed as a call argument, which no context
|
|
137
|
+
// reaches, so `it('...', () => {})` is never asked to carry a block.
|
|
128
138
|
files: ['**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', '**/__tests__/**', '**/tests/**', '**/test-support/**'],
|
|
129
139
|
rules: {
|
|
130
140
|
'max-lines-per-function': 'off',
|
|
131
141
|
'max-depth': 'off',
|
|
132
|
-
'jsdoc/require-jsdoc': 'off',
|
|
133
142
|
},
|
|
134
143
|
},
|
|
135
144
|
];
|
package/js/eslint/plugin.js
CHANGED
|
@@ -7,6 +7,7 @@ import noBaseError from './rules/no-base-error.js';
|
|
|
7
7
|
import noInterfacePrefix from './rules/no-interface-prefix.js';
|
|
8
8
|
import noMutableStatic from './rules/no-mutable-static.js';
|
|
9
9
|
import requireCopyright from './rules/require-copyright.js';
|
|
10
|
+
import requireFileDescription from './rules/require-file-description.js';
|
|
10
11
|
import requireReadonlyPublicProperty from './rules/require-readonly-public-property.js';
|
|
11
12
|
import singleLinePropertyDoc from './rules/single-line-property-doc.js';
|
|
12
13
|
import validEnumMemberName from './rules/valid-enum-member-name.js';
|
|
@@ -34,6 +35,7 @@ export default {
|
|
|
34
35
|
'max-methods-per-class': maxMethodsPerClass,
|
|
35
36
|
'no-base-error': noBaseError,
|
|
36
37
|
'require-copyright': requireCopyright,
|
|
38
|
+
'require-file-description': requireFileDescription,
|
|
37
39
|
'align-doc-tags': alignDocTags,
|
|
38
40
|
'single-line-property-doc': singleLinePropertyDoc,
|
|
39
41
|
'multiline-function-doc': multilineFunctionDoc,
|
package/js/eslint/rules/lib.js
CHANGED
|
@@ -92,3 +92,27 @@ export function isTestClass(klass) {
|
|
|
92
92
|
|
|
93
93
|
return parent !== null && parent.endsWith('TestCase');
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
/** A boundary-anchored matcher for a documentation tag by its bare name. */
|
|
97
|
+
export function tagMatcher(tag) {
|
|
98
|
+
const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
|
|
100
|
+
return new RegExp(`(?:^|[\\s*])@${escaped}(?![-\\w])`, 'i');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The file's descriptive docblock: the block comment carrying all of the given
|
|
105
|
+
* tags together, or null when no comment carries them.
|
|
106
|
+
*
|
|
107
|
+
* The block is found by its tags rather than its position, since the header
|
|
108
|
+
* need not open the file: a module documented at its export sits below the
|
|
109
|
+
* imports. Requiring one comment to carry every tag is what makes the match a
|
|
110
|
+
* single block rather than a header split across several.
|
|
111
|
+
*/
|
|
112
|
+
export function fileDocBlock(sourceCode, tags) {
|
|
113
|
+
const matchers = tags.map(tagMatcher);
|
|
114
|
+
|
|
115
|
+
return sourceCode.getAllComments().find(
|
|
116
|
+
comment => comment.type === 'Block' && matchers.every(matcher => matcher.test(comment.value)),
|
|
117
|
+
) ?? null;
|
|
118
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import { createRule } from './lib.js';
|
|
1
|
+
import { createRule, fileDocBlock } from './lib.js';
|
|
2
2
|
|
|
3
3
|
const DEFAULT_TAGS = ['copyright', 'author'];
|
|
4
4
|
|
|
5
|
-
/** A boundary-anchored matcher for a documentation tag by its bare name. */
|
|
6
|
-
function tagMatcher(tag) {
|
|
7
|
-
const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
-
|
|
9
|
-
return new RegExp(`(?:^|[\\s*])@${escaped}(?![-\\w])`, 'i');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
5
|
/**
|
|
13
6
|
* Require a documentation comment carrying the tags every source file must
|
|
14
7
|
* declare, `@copyright` and `@author` by default.
|
|
@@ -17,7 +10,9 @@ function tagMatcher(tag) {
|
|
|
17
10
|
* sit inside the file's descriptive docblock alongside its summary rather than
|
|
18
11
|
* in a separate header. Only the presence of each tag is checked, never its
|
|
19
12
|
* value or alignment, which are matters of formatting. The required set is
|
|
20
|
-
* configurable, so a project may drop `@author` or add tags of its own.
|
|
13
|
+
* configurable, so a project may drop `@author` or add tags of its own. The
|
|
14
|
+
* summary those tags sit beside is the separate concern of
|
|
15
|
+
* `require-file-description`, which locates the same block by the same tags.
|
|
21
16
|
*
|
|
22
17
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
23
18
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -49,15 +44,10 @@ export default createRule({
|
|
|
49
44
|
create(context, [options]) {
|
|
50
45
|
const { sourceCode } = context;
|
|
51
46
|
const required = options.tags ?? DEFAULT_TAGS;
|
|
52
|
-
const matchers = required.map(tagMatcher);
|
|
53
47
|
|
|
54
48
|
return {
|
|
55
49
|
Program(node) {
|
|
56
|
-
|
|
57
|
-
comment => comment.type === 'Block' && matchers.every(matcher => matcher.test(comment.value)),
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
if (documented) {
|
|
50
|
+
if (fileDocBlock(sourceCode, required) !== null) {
|
|
61
51
|
return;
|
|
62
52
|
}
|
|
63
53
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createRule, fileDocBlock } from './lib.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TAGS = ['copyright', 'author'];
|
|
4
|
+
|
|
5
|
+
/** A line opening a documentation tag rather than carrying prose. */
|
|
6
|
+
const TAG_LINE = /^@[A-Za-z][\w-]*(?:\s|$)/;
|
|
7
|
+
|
|
8
|
+
/** The prose a documentation line carries, with its leading margin removed. */
|
|
9
|
+
function content(line) {
|
|
10
|
+
return line.replace(/^\s*\*\s*/, '').trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Whether the block opens with prose rather than going straight to its tags. */
|
|
14
|
+
function described(comment) {
|
|
15
|
+
for (const line of comment.value.split('\n')) {
|
|
16
|
+
const text = content(line);
|
|
17
|
+
|
|
18
|
+
if (text === '') {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return !TAG_LINE.test(text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Require the file's descriptive docblock to carry a summary, not tags alone.
|
|
30
|
+
*
|
|
31
|
+
* The house convention is a prose summary followed by the tags every file
|
|
32
|
+
* declares, so `@author` and `@copyright` annotate a description rather than
|
|
33
|
+
* standing as a header of their own. `require-copyright` has always asked for
|
|
34
|
+
* the tags; this asks for the sentence they were meant to sit beneath, which
|
|
35
|
+
* was the intent all along and the half nothing enforced. A file whose entire
|
|
36
|
+
* header is two tags says who owns it and nothing about what it is.
|
|
37
|
+
*
|
|
38
|
+
* The block is located exactly as `require-copyright` locates it, by the tags
|
|
39
|
+
* it carries, so the two rules always speak about the same comment; a project
|
|
40
|
+
* changing one rule's `tags` should change the other's to match. A file with no
|
|
41
|
+
* such block is left alone, since `require-copyright` already owns the block's
|
|
42
|
+
* existence and faulting one omission twice would only double the report.
|
|
43
|
+
*
|
|
44
|
+
* Only the block's first non-blank line is read, and only whether it is prose.
|
|
45
|
+
* That places the summary above the tags, as the convention writes it, and
|
|
46
|
+
* keeps a wrapped tag value's continuation line from passing as a description.
|
|
47
|
+
* What the sentence says is the author's business: the rule asserts that prose
|
|
48
|
+
* is there, never that it is good.
|
|
49
|
+
*
|
|
50
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
51
|
+
* @copyright 2026 Sine Macula Limited
|
|
52
|
+
*/
|
|
53
|
+
export default createRule({
|
|
54
|
+
name: 'require-file-description',
|
|
55
|
+
meta: {
|
|
56
|
+
type: 'suggestion',
|
|
57
|
+
docs: {
|
|
58
|
+
description: "Require the file's documentation comment to open with a description.",
|
|
59
|
+
},
|
|
60
|
+
schema: [
|
|
61
|
+
{
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
tags: {
|
|
65
|
+
type: 'array',
|
|
66
|
+
items: { type: 'string' },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
messages: {
|
|
73
|
+
missing: 'The documentation comment carrying {{ tags }} must open with a description.',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
defaultOptions: [{ tags: DEFAULT_TAGS }],
|
|
77
|
+
create(context, [options]) {
|
|
78
|
+
const { sourceCode } = context;
|
|
79
|
+
const required = options.tags ?? DEFAULT_TAGS;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
Program() {
|
|
83
|
+
const doc = fileDocBlock(sourceCode, required);
|
|
84
|
+
|
|
85
|
+
if (doc === null || described(doc)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
context.report({
|
|
90
|
+
node: doc,
|
|
91
|
+
messageId: 'missing',
|
|
92
|
+
data: { tags: required.map(tag => `@${tag}`).join(', ') },
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sinemacula/coding-standards",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.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>",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"eslint-plugin-vue": "^10.0.0",
|
|
59
59
|
"typescript": "^5.0.0",
|
|
60
60
|
"typescript-eslint": "^8.0.0",
|
|
61
|
-
"vitest": "^
|
|
61
|
+
"vitest": "^4.1.11",
|
|
62
62
|
"vue-eslint-parser": "^10.0.0",
|
|
63
63
|
"yaml-eslint-parser": "^2.1.0"
|
|
64
64
|
},
|