@starci/eslint-canon-be 1.2.0 → 1.2.2
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/CHANGELOG.md +7 -0
- package/README.md +70 -69
- package/authorization.mjs +106 -106
- package/cdc.mjs +150 -150
- package/comments.mjs +388 -389
- package/cqrs.mjs +302 -303
- package/data-access.mjs +306 -307
- package/e2e-flow.mjs +222 -222
- package/event-delivery.mjs +93 -93
- package/exception-identity.mjs +200 -200
- package/exceptions.mjs +262 -265
- package/index.mjs +107 -107
- package/lib/path.mjs +2 -0
- package/module-layering.mjs +426 -432
- package/naming.mjs +219 -220
- package/observability.mjs +275 -275
- package/package.json +4 -2
- package/testing.mjs +500 -454
- package/transport.mjs +168 -168
- package/type-safety.mjs +510 -380
package/naming.mjs
CHANGED
|
@@ -1,220 +1,219 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold `naming.md`.
|
|
3
|
-
*
|
|
4
|
-
* Three rules, which are the parts of naming a parser can actually judge: whether a name encodes a
|
|
5
|
-
* schema VERSION, whether an exported function is a bare verb, and whether a `.module.ts` file's
|
|
6
|
-
* OWN static factory borrows a vendor dynamic-module's exact name instead of this tree's `register`.
|
|
7
|
-
*
|
|
8
|
-
* A FOURTH RULE WAS WRITTEN AND DELETED, and the deletion is the useful part. It demanded that a
|
|
9
|
-
* file name spell out the class it declares, and measured 616 offenders in 4430 files -- because
|
|
10
|
-
* the convention is the opposite of what it assumed: the PATH carries the role and the scope
|
|
11
|
-
* (`parsers/content.service.ts` and `path/content.service.ts` declare `ContentParserService` and
|
|
12
|
-
* `ContentPathService`), and the file names only the subject. Fourteen percent of a tree is a
|
|
13
|
-
* convention, not debt. Canon records what the code does; a rule that disagrees with the code at
|
|
14
|
-
* that scale is the rule being wrong.
|
|
15
|
-
*
|
|
16
|
-
* The module-factory rule (NAME-1) stays narrow on purpose: it visits DECLARATIONS only -- a static
|
|
17
|
-
* method on a class this repository declares, decorated `@Module(...)` -- and never a CallExpression,
|
|
18
|
-
* so `BullModule.forRoot(...)`, `ConfigModule.forRoot(...)`, `TypeOrmModule.forFeature(...)` and
|
|
19
|
-
* `SentryCoreModule.forRoot(...)` are calls into vendor code and are never reported. Its forbidden
|
|
20
|
-
* set is closed to the four literal vendor names (`forRoot`, `forRootAsync`, `forFeature`,
|
|
21
|
-
* `forFeatureAsync`); a repository-invented auxiliary factory like `registerQueue` does not collide
|
|
22
|
-
* with a vendor shape and is left alone.
|
|
23
|
-
*
|
|
24
|
-
* The rest of the law is not enforced and should not be faked. Whether a name describes a folder
|
|
25
|
-
* (NAME-3), a mechanism (NAME-4) or its first caller (NAME-7) needs to know what the thing IS --
|
|
26
|
-
* `VolumeService` reads perfectly well until you know the folder was renamed, and no parser knows
|
|
27
|
-
* that. Those are read by a person, which is why the law states them with the scars attached.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (expression.type === "
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
["
|
|
82
|
-
["
|
|
83
|
-
["
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (!node.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"no-
|
|
203
|
-
"no-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"starci-be/no-
|
|
218
|
-
"starci-be/no-
|
|
219
|
-
|
|
220
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `naming.md`.
|
|
3
|
+
*
|
|
4
|
+
* Three rules, which are the parts of naming a parser can actually judge: whether a name encodes a
|
|
5
|
+
* schema VERSION, whether an exported function is a bare verb, and whether a `.module.ts` file's
|
|
6
|
+
* OWN static factory borrows a vendor dynamic-module's exact name instead of this tree's `register`.
|
|
7
|
+
*
|
|
8
|
+
* A FOURTH RULE WAS WRITTEN AND DELETED, and the deletion is the useful part. It demanded that a
|
|
9
|
+
* file name spell out the class it declares, and measured 616 offenders in 4430 files -- because
|
|
10
|
+
* the convention is the opposite of what it assumed: the PATH carries the role and the scope
|
|
11
|
+
* (`parsers/content.service.ts` and `path/content.service.ts` declare `ContentParserService` and
|
|
12
|
+
* `ContentPathService`), and the file names only the subject. Fourteen percent of a tree is a
|
|
13
|
+
* convention, not debt. Canon records what the code does; a rule that disagrees with the code at
|
|
14
|
+
* that scale is the rule being wrong.
|
|
15
|
+
*
|
|
16
|
+
* The module-factory rule (NAME-1) stays narrow on purpose: it visits DECLARATIONS only -- a static
|
|
17
|
+
* method on a class this repository declares, decorated `@Module(...)` -- and never a CallExpression,
|
|
18
|
+
* so `BullModule.forRoot(...)`, `ConfigModule.forRoot(...)`, `TypeOrmModule.forFeature(...)` and
|
|
19
|
+
* `SentryCoreModule.forRoot(...)` are calls into vendor code and are never reported. Its forbidden
|
|
20
|
+
* set is closed to the four literal vendor names (`forRoot`, `forRootAsync`, `forFeature`,
|
|
21
|
+
* `forFeatureAsync`); a repository-invented auxiliary factory like `registerQueue` does not collide
|
|
22
|
+
* with a vendor shape and is left alone.
|
|
23
|
+
*
|
|
24
|
+
* The rest of the law is not enforced and should not be faked. Whether a name describes a folder
|
|
25
|
+
* (NAME-3), a mechanism (NAME-4) or its first caller (NAME-7) needs to know what the thing IS --
|
|
26
|
+
* `VolumeService` reads perfectly well until you know the folder was renamed, and no parser knows
|
|
27
|
+
* that. Those are read by a person, which is why the law states them with the scars attached.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { normalizePath } from "./lib/path.mjs"
|
|
31
|
+
|
|
32
|
+
/** A schema generation baked into an identifier. */
|
|
33
|
+
const VERSIONED_NAME = /(?:^|[a-z])V[0-9]+(?:$|[A-Z_])|_V[0-9]+/
|
|
34
|
+
|
|
35
|
+
/** Bare verbs that say nothing about their object. */
|
|
36
|
+
const BARE_VERBS = new Set([
|
|
37
|
+
"generate",
|
|
38
|
+
"parse",
|
|
39
|
+
"run",
|
|
40
|
+
"handle",
|
|
41
|
+
"process",
|
|
42
|
+
"build",
|
|
43
|
+
"create",
|
|
44
|
+
"load",
|
|
45
|
+
"resolve",
|
|
46
|
+
"check",
|
|
47
|
+
"convert",
|
|
48
|
+
"transform",
|
|
49
|
+
"send",
|
|
50
|
+
"fetch",
|
|
51
|
+
"get",
|
|
52
|
+
"set",
|
|
53
|
+
"update",
|
|
54
|
+
"apply",
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
// -- NAME-1 ----------------------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/** Decorator names carried by a class declaration. */
|
|
60
|
+
const decoratorNames = (node) =>
|
|
61
|
+
(node.decorators || []).map((decorator) => {
|
|
62
|
+
const expression = decorator.expression
|
|
63
|
+
if (expression.type === "CallExpression" && expression.callee.type === "Identifier") return expression.callee.name
|
|
64
|
+
if (expression.type === "Identifier") return expression.name
|
|
65
|
+
return null
|
|
66
|
+
}).filter(Boolean)
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Vendor dynamic-module factory names, each mapped to the house name a `.module.ts` file's own
|
|
70
|
+
* factory declares instead: `register`, or `registerAsync` for its async companion.
|
|
71
|
+
*
|
|
72
|
+
* This is a CLOSED list of the exact method names Nest's own modules publish
|
|
73
|
+
* (`ConfigModule.forRoot`, `TypeOrmModule.forFeature`, `BullModule.forRootAsync`,
|
|
74
|
+
* `SentryCoreModule.forRoot`). A repository class that declares its OWN static factory under one of
|
|
75
|
+
* these names is not calling the vendor - it is shaped like the vendor, which is exactly what makes
|
|
76
|
+
* `PrimaryPostgreSQLModule.forFeature()` unreadable at a glance from `TypeOrmModule.forFeature()`:
|
|
77
|
+
* same literal name, two different owners, and nothing at the call site says which.
|
|
78
|
+
*/
|
|
79
|
+
const VENDOR_FACTORY_NAMES = new Map([
|
|
80
|
+
["forRoot", "register"],
|
|
81
|
+
["forRootAsync", "registerAsync"],
|
|
82
|
+
["forFeature", "register"],
|
|
83
|
+
["forFeatureAsync", "registerAsync"],
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
/** A `.module.ts` file's own static factory is named `register`, never a vendor's factory shape. */
|
|
87
|
+
export const noVendorModuleFactoryName = {
|
|
88
|
+
meta: {
|
|
89
|
+
type: "problem",
|
|
90
|
+
docs: {
|
|
91
|
+
description:
|
|
92
|
+
"NAME-1: a `.module.ts` file's own static factory is named `register` (`registerAsync` for its async companion), never a vendor dynamic-module name it does not own.",
|
|
93
|
+
},
|
|
94
|
+
schema: [],
|
|
95
|
+
messages: {
|
|
96
|
+
vendorShaped:
|
|
97
|
+
"`{{name}}` is a module declared IN THIS REPOSITORY, but its static factory is named `{{method}}` - the exact name Nest's own vendor modules publish (`ConfigModule.forRoot`, `TypeOrmModule.forFeature`, `BullModule.forRootAsync`). A reader hitting `{{name}}.{{method}}(...)` in an `imports:` list cannot tell, from the name alone, whether that call lands in this class or forwards straight into a vendor's. Thirty-nine other modules in this tree already answer that question by calling this factory `{{suggestion}}` instead - a name no vendor already owns. Rename `{{method}}` to `{{suggestion}}`.",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
create(context) {
|
|
101
|
+
return {
|
|
102
|
+
MethodDefinition(node) {
|
|
103
|
+
if (!node.static) return
|
|
104
|
+
if (!node.key || node.key.type !== "Identifier") return
|
|
105
|
+
const suggestion = VENDOR_FACTORY_NAMES.get(node.key.name)
|
|
106
|
+
if (!suggestion) return
|
|
107
|
+
|
|
108
|
+
const classBody = node.parent
|
|
109
|
+
const classNode = classBody && classBody.parent
|
|
110
|
+
if (!classNode || !decoratorNames(classNode).includes("Module")) return
|
|
111
|
+
|
|
112
|
+
const name = (classNode.id && classNode.id.name) || "this module"
|
|
113
|
+
context.report({
|
|
114
|
+
node: node.key,
|
|
115
|
+
messageId: "vendorShaped",
|
|
116
|
+
data: { name, method: node.key.name, suggestion },
|
|
117
|
+
})
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -- NAME-2 ----------------------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/** A name says what a thing is, never which schema generation it was built for. */
|
|
126
|
+
export const noVersionInName = {
|
|
127
|
+
meta: {
|
|
128
|
+
type: "problem",
|
|
129
|
+
docs: { description: "Identifiers do not encode a schema version." },
|
|
130
|
+
schema: [],
|
|
131
|
+
messages: {
|
|
132
|
+
versioned:
|
|
133
|
+
"`{{name}}` bakes a schema generation into the name. It has to be renamed the day the next one arrives, and until then nobody can tell from the name whether it means the CURRENT shape or an old one -- so every reader goes and finds out. Name the property that generation introduced instead: `hasVerifiedMarker`, not `isV2`.",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
create(context) {
|
|
137
|
+
const report = (node, name) => {
|
|
138
|
+
if (!name || !VERSIONED_NAME.test(name)) return
|
|
139
|
+
context.report({ node, messageId: "versioned", data: { name } })
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
FunctionDeclaration(node) {
|
|
143
|
+
if (node.id) report(node.id, node.id.name)
|
|
144
|
+
},
|
|
145
|
+
ClassDeclaration(node) {
|
|
146
|
+
if (node.id) report(node.id, node.id.name)
|
|
147
|
+
},
|
|
148
|
+
TSInterfaceDeclaration(node) {
|
|
149
|
+
if (node.id) report(node.id, node.id.name)
|
|
150
|
+
},
|
|
151
|
+
TSTypeAliasDeclaration(node) {
|
|
152
|
+
if (node.id) report(node.id, node.id.name)
|
|
153
|
+
},
|
|
154
|
+
MethodDefinition(node) {
|
|
155
|
+
if (node.key && node.key.type === "Identifier") report(node.key, node.key.name)
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// -- NAME-5 ----------------------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/** An exported function is a verb plus its object. */
|
|
164
|
+
export const noBareVerbExport = {
|
|
165
|
+
meta: {
|
|
166
|
+
type: "problem",
|
|
167
|
+
docs: { description: "An exported function names its object, not just its verb." },
|
|
168
|
+
schema: [],
|
|
169
|
+
messages: {
|
|
170
|
+
bareVerb:
|
|
171
|
+
"`{{name}}` is a bare verb: {{name}} WHAT? At an import list it collides with every other module's `{{name}}`, so the reader falls back on reading the path to tell them apart -- and the path is the thing that moves. Give it its object.",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
create(context) {
|
|
175
|
+
const check = (node, name) => {
|
|
176
|
+
if (!name || !BARE_VERBS.has(name)) return
|
|
177
|
+
context.report({ node, messageId: "bareVerb", data: { name } })
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
ExportNamedDeclaration(node) {
|
|
181
|
+
const declaration = node.declaration
|
|
182
|
+
if (!declaration) return
|
|
183
|
+
if (declaration.type === "FunctionDeclaration" && declaration.id) {
|
|
184
|
+
check(declaration.id, declaration.id.name)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
if (declaration.type !== "VariableDeclaration") return
|
|
188
|
+
for (const one of declaration.declarations) {
|
|
189
|
+
const init = one.init
|
|
190
|
+
const isFunction = init
|
|
191
|
+
&& (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")
|
|
192
|
+
if (isFunction && one.id && one.id.type === "Identifier") check(one.id, one.id.name)
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The rules this law contributes to the plugin. */
|
|
200
|
+
export const rules = {
|
|
201
|
+
"no-version-in-name": noVersionInName,
|
|
202
|
+
"no-bare-verb-export": noBareVerbExport,
|
|
203
|
+
"no-vendor-module-factory-name": noVendorModuleFactoryName,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The level this law asks for, as the plugin's own opinion.
|
|
208
|
+
*
|
|
209
|
+
* MEASURE THESE BEFORE SWITCHING THEM ON. Unlike the other backend laws, naming rules land on a
|
|
210
|
+
* mature tree with real debt: a repository that has been running for a while will have file names
|
|
211
|
+
* that drifted from their exports and methods carrying a schema generation. Land above zero at
|
|
212
|
+
* `warn` with the count and burn it down -- a naming rule at `error` on day one blocks every commit
|
|
213
|
+
* that touches an old file, which teaches people to disable it.
|
|
214
|
+
*/
|
|
215
|
+
export const recommended = {
|
|
216
|
+
"starci-be/no-version-in-name": "warn",
|
|
217
|
+
"starci-be/no-bare-verb-export": "warn",
|
|
218
|
+
"starci-be/no-vendor-module-factory-name": "warn",
|
|
219
|
+
}
|