@sinemacula/coding-standards 1.8.4 → 1.9.1
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 +75 -1
- package/js/biome.json +15 -1
- package/js/eslint/index.js +86 -0
- package/js/eslint/plugin.js +34 -0
- package/js/eslint/rules/boolean-method-name.js +315 -0
- package/js/eslint/rules/lib.js +90 -0
- package/js/eslint/rules/max-methods-per-class.js +92 -0
- package/js/eslint/rules/no-base-error.js +120 -0
- package/js/eslint/rules/no-interface-prefix.js +55 -0
- package/js/eslint/rules/no-mutable-static.js +213 -0
- package/js/eslint/rules/require-copyright.js +73 -0
- package/js/eslint/rules/require-readonly-public-property.js +154 -0
- package/js/eslint/rules/valid-enum-member-name.js +46 -0
- package/js/eslint/type-checked.js +41 -0
- package/package.json +43 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRule,
|
|
3
|
+
isAmbient,
|
|
4
|
+
isDeclarationFile,
|
|
5
|
+
isTestClass,
|
|
6
|
+
isTestPath,
|
|
7
|
+
nearestClass,
|
|
8
|
+
superClassName,
|
|
9
|
+
} from './lib.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Require the `readonly` modifier on public class properties.
|
|
13
|
+
*
|
|
14
|
+
* Public properties, whether declared or constructor-promoted, must be
|
|
15
|
+
* `readonly`. Mutable public state breaks encapsulation; the legitimate
|
|
16
|
+
* data-holder case is expressed with `public readonly`. Public auto-accessors
|
|
17
|
+
* have no `readonly` form and always expose a setter, so they are disallowed
|
|
18
|
+
* outright. Static properties are left to the mutable static state concern,
|
|
19
|
+
* non-public properties are unaffected, and ambient declarations plus test
|
|
20
|
+
* fixtures are exempt. TypeScript has no whole-class readonly modifier, so
|
|
21
|
+
* there is no class-level exemption.
|
|
22
|
+
*
|
|
23
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
24
|
+
* @copyright 2026 Sine Macula Limited
|
|
25
|
+
*/
|
|
26
|
+
export default createRule({
|
|
27
|
+
name: 'require-readonly-public-property',
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'problem',
|
|
30
|
+
docs: {
|
|
31
|
+
description: 'Require the readonly modifier on public class properties.',
|
|
32
|
+
},
|
|
33
|
+
schema: [
|
|
34
|
+
{
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
ignoredParentClasses: {
|
|
38
|
+
type: 'array',
|
|
39
|
+
items: { type: 'string' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
messages: {
|
|
46
|
+
mutable: 'Public property "{{ name }}" must be readonly; mutable public state breaks encapsulation.',
|
|
47
|
+
accessor: 'Public auto-accessor "{{ name }}" is mutable; make it non-public or a readonly property.',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
defaultOptions: [{ ignoredParentClasses: [] }],
|
|
51
|
+
create(context, [options]) {
|
|
52
|
+
const ignoredParents = options.ignoredParentClasses ?? [];
|
|
53
|
+
const { sourceCode } = context;
|
|
54
|
+
|
|
55
|
+
// Declaration files and test directories are exempt wholesale.
|
|
56
|
+
if (isDeclarationFile(context.filename) || isTestPath(context.filename)) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether the node's enclosing class is exempt from the mandate. */
|
|
61
|
+
const isExempt = node => {
|
|
62
|
+
if (isAmbient(node, context.filename)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const klass = nearestClass(sourceCode.getAncestors(node));
|
|
67
|
+
|
|
68
|
+
return klass !== null
|
|
69
|
+
&& (isTestClass(klass) || isIgnoredParent(klass, ignoredParents));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
'PropertyDefinition, TSAbstractPropertyDefinition'(node) {
|
|
74
|
+
if (isOutOfScope(node) || node.readonly || isExempt(node)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
context.report({
|
|
79
|
+
node: node.key,
|
|
80
|
+
messageId: 'mutable',
|
|
81
|
+
data: { name: propertyName(node.key, sourceCode) },
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
'AccessorProperty, TSAbstractAccessorProperty'(node) {
|
|
85
|
+
// An auto-accessor cannot be readonly, so a public one is always mutable.
|
|
86
|
+
if (isOutOfScope(node) || isExempt(node)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
context.report({
|
|
91
|
+
node: node.key,
|
|
92
|
+
messageId: 'accessor',
|
|
93
|
+
data: { name: propertyName(node.key, sourceCode) },
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
TSParameterProperty(node) {
|
|
97
|
+
if (node.readonly || node.accessibility !== 'public' || isExempt(node)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
context.report({
|
|
102
|
+
node: node.parameter,
|
|
103
|
+
messageId: 'mutable',
|
|
104
|
+
data: { name: parameterName(node.parameter) },
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Whether the property's accessibility marks it non-public. */
|
|
112
|
+
function isNonPublic(node) {
|
|
113
|
+
return node.key.type === 'PrivateIdentifier'
|
|
114
|
+
|| node.accessibility === 'private'
|
|
115
|
+
|| node.accessibility === 'protected';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether a class property is outside the public-mutable scope: static, ambient, or non-public. */
|
|
119
|
+
function isOutOfScope(node) {
|
|
120
|
+
return node.static || node.declare || isNonPublic(node);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether the class extends one of the configured exempt parents. */
|
|
124
|
+
function isIgnoredParent(klass, ignoredParents) {
|
|
125
|
+
const parent = superClassName(klass);
|
|
126
|
+
|
|
127
|
+
return parent !== null && ignoredParents.includes(parent);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A readable name for a (possibly computed) property key. */
|
|
131
|
+
function propertyName(key, sourceCode) {
|
|
132
|
+
if (key.type === 'Identifier') {
|
|
133
|
+
return key.name;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (key.type === 'Literal') {
|
|
137
|
+
return String(key.value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return sourceCode.getText(key);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The declared name of a constructor parameter property. */
|
|
144
|
+
function parameterName(parameter) {
|
|
145
|
+
if (parameter.type === 'Identifier') {
|
|
146
|
+
return parameter.name;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (parameter.type === 'AssignmentPattern' && parameter.left.type === 'Identifier') {
|
|
150
|
+
return parameter.left.name;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRule } from './lib.js';
|
|
2
|
+
|
|
3
|
+
/** The pattern a valid enum member name must match. */
|
|
4
|
+
const NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Enum member naming rule.
|
|
8
|
+
*
|
|
9
|
+
* Ensures every enum member is declared in SCREAMING_SNAKE_CASE, matching the
|
|
10
|
+
* Sine Macula house convention for enum members. Only real `enum` declarations
|
|
11
|
+
* are checked; object-literal "as const" pseudo-enums are out of scope.
|
|
12
|
+
*
|
|
13
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
14
|
+
* @copyright 2026 Sine Macula Limited
|
|
15
|
+
*/
|
|
16
|
+
export default createRule({
|
|
17
|
+
name: 'valid-enum-member-name',
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'suggestion',
|
|
20
|
+
docs: {
|
|
21
|
+
description: 'Require enum members to be declared in SCREAMING_SNAKE_CASE.',
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
notUpperSnakeCase: 'Enum member "{{ name }}" must be declared in SCREAMING_SNAKE_CASE.',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultOptions: [],
|
|
29
|
+
create(context) {
|
|
30
|
+
// Only enum members are visited; a switch `case` is a SwitchCase node
|
|
31
|
+
// and never reaches this visitor.
|
|
32
|
+
return {
|
|
33
|
+
TSEnumMember(node) {
|
|
34
|
+
const id = node.id;
|
|
35
|
+
// A member name is either a bare identifier or a string literal;
|
|
36
|
+
// a string literal is checked by its resolved value, not the raw
|
|
37
|
+
// source text.
|
|
38
|
+
const name = id.type === 'Identifier' ? id.name : String(id.value);
|
|
39
|
+
|
|
40
|
+
if (!NAME_PATTERN.test(name)) {
|
|
41
|
+
context.report({ node: id, messageId: 'notUpperSnakeCase', data: { name } });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import tseslint from 'typescript-eslint';
|
|
2
|
+
import base from './index.js';
|
|
3
|
+
import plugin from './plugin.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in type-aware layer: the cross-file and type-driven rules. Spreads the
|
|
7
|
+
* base config, then turns on type information via the project service so the
|
|
8
|
+
* type-aware custom rules and the curated typescript-eslint rules can resolve.
|
|
9
|
+
* Layered on only where a consumer tsconfig exists, keeping the base config the
|
|
10
|
+
* cheap fast path.
|
|
11
|
+
*
|
|
12
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
13
|
+
* @copyright 2026 Sine Macula Limited
|
|
14
|
+
*/
|
|
15
|
+
export default [
|
|
16
|
+
...base,
|
|
17
|
+
{
|
|
18
|
+
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
|
19
|
+
plugins: {
|
|
20
|
+
'@sinemacula': plugin,
|
|
21
|
+
'@typescript-eslint': tseslint.plugin,
|
|
22
|
+
},
|
|
23
|
+
languageOptions: {
|
|
24
|
+
parser: tseslint.parser,
|
|
25
|
+
parserOptions: {
|
|
26
|
+
projectService: true,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
rules: {
|
|
30
|
+
'@sinemacula/boolean-method-name': 'error',
|
|
31
|
+
|
|
32
|
+
'@typescript-eslint/await-thenable': 'error',
|
|
33
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
34
|
+
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
|
35
|
+
'@typescript-eslint/no-floating-promises': 'error',
|
|
36
|
+
'@typescript-eslint/no-misused-promises': 'error',
|
|
37
|
+
'@typescript-eslint/only-throw-error': 'error',
|
|
38
|
+
// '@typescript-eslint/strict-boolean-expressions': 'error',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sinemacula/coding-standards",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
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>",
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
"keywords": [
|
|
16
16
|
"biome",
|
|
17
17
|
"knip",
|
|
18
|
+
"eslint",
|
|
18
19
|
"coding-standards",
|
|
19
20
|
"linting",
|
|
20
21
|
"config"
|
|
21
22
|
],
|
|
23
|
+
"type": "module",
|
|
22
24
|
"files": [
|
|
23
25
|
"js/",
|
|
24
26
|
"markdown/",
|
|
@@ -27,8 +29,47 @@
|
|
|
27
29
|
"security/",
|
|
28
30
|
"README.md",
|
|
29
31
|
"LICENSE",
|
|
30
|
-
"NOTICE"
|
|
32
|
+
"NOTICE",
|
|
33
|
+
"!js/eslint/rules/__tests__"
|
|
31
34
|
],
|
|
35
|
+
"exports": {
|
|
36
|
+
"./js/eslint": "./js/eslint/index.js",
|
|
37
|
+
"./js/eslint/type-checked": "./js/eslint/type-checked.js",
|
|
38
|
+
"./*": "./*"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test:js": "vitest run",
|
|
42
|
+
"lint:js": "eslint js/eslint"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
46
|
+
"@typescript-eslint/utils": "^8.0.0",
|
|
47
|
+
"eslint": "^9.0.0",
|
|
48
|
+
"eslint-plugin-jsdoc": "^63.0.13",
|
|
49
|
+
"typescript": "^5.0.0",
|
|
50
|
+
"typescript-eslint": "^8.0.0",
|
|
51
|
+
"vitest": "^3.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"eslint": ">=9",
|
|
55
|
+
"eslint-plugin-jsdoc": ">=48",
|
|
56
|
+
"typescript": ">=4.8.4",
|
|
57
|
+
"typescript-eslint": "^8"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"eslint": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"eslint-plugin-jsdoc": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"typescript": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"typescript-eslint": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
32
73
|
"publishConfig": {
|
|
33
74
|
"access": "public",
|
|
34
75
|
"registry": "https://registry.npmjs.org/"
|