@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/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
- /** Forward-slash form of a filename, so Windows paths compare like every other path. */
31
- const normalizePath = (filename) => String(filename || "").replace(/\\/g, "/")
32
-
33
- /** A schema generation baked into an identifier. */
34
- const VERSIONED_NAME = /(?:^|[a-z])V[0-9]+(?:$|[A-Z_])|_V[0-9]+/
35
-
36
- /** Bare verbs that say nothing about their object. */
37
- const BARE_VERBS = new Set([
38
- "generate",
39
- "parse",
40
- "run",
41
- "handle",
42
- "process",
43
- "build",
44
- "create",
45
- "load",
46
- "resolve",
47
- "check",
48
- "convert",
49
- "transform",
50
- "send",
51
- "fetch",
52
- "get",
53
- "set",
54
- "update",
55
- "apply",
56
- ])
57
-
58
- // -- NAME-1 ----------------------------------------------------------------------------------------
59
-
60
- /** Decorator names carried by a class declaration. */
61
- const decoratorNames = (node) =>
62
- (node.decorators || []).map((decorator) => {
63
- const expression = decorator.expression
64
- if (expression.type === "CallExpression" && expression.callee.type === "Identifier") return expression.callee.name
65
- if (expression.type === "Identifier") return expression.name
66
- return null
67
- }).filter(Boolean)
68
-
69
- /**
70
- * Vendor dynamic-module factory names, each mapped to the house name a `.module.ts` file's own
71
- * factory declares instead: `register`, or `registerAsync` for its async companion.
72
- *
73
- * This is a CLOSED list of the exact method names Nest's own modules publish
74
- * (`ConfigModule.forRoot`, `TypeOrmModule.forFeature`, `BullModule.forRootAsync`,
75
- * `SentryCoreModule.forRoot`). A repository class that declares its OWN static factory under one of
76
- * these names is not calling the vendor - it is shaped like the vendor, which is exactly what makes
77
- * `PrimaryPostgreSQLModule.forFeature()` unreadable at a glance from `TypeOrmModule.forFeature()`:
78
- * same literal name, two different owners, and nothing at the call site says which.
79
- */
80
- const VENDOR_FACTORY_NAMES = new Map([
81
- ["forRoot", "register"],
82
- ["forRootAsync", "registerAsync"],
83
- ["forFeature", "register"],
84
- ["forFeatureAsync", "registerAsync"],
85
- ])
86
-
87
- /** A `.module.ts` file's own static factory is named `register`, never a vendor's factory shape. */
88
- export const noVendorModuleFactoryName = {
89
- meta: {
90
- type: "problem",
91
- docs: {
92
- description:
93
- "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.",
94
- },
95
- schema: [],
96
- messages: {
97
- vendorShaped:
98
- "`{{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}}`.",
99
- },
100
- },
101
- create(context) {
102
- return {
103
- MethodDefinition(node) {
104
- if (!node.static) return
105
- if (!node.key || node.key.type !== "Identifier") return
106
- const suggestion = VENDOR_FACTORY_NAMES.get(node.key.name)
107
- if (!suggestion) return
108
-
109
- const classBody = node.parent
110
- const classNode = classBody && classBody.parent
111
- if (!classNode || !decoratorNames(classNode).includes("Module")) return
112
-
113
- const name = (classNode.id && classNode.id.name) || "this module"
114
- context.report({
115
- node: node.key,
116
- messageId: "vendorShaped",
117
- data: { name, method: node.key.name, suggestion },
118
- })
119
- },
120
- }
121
- },
122
- }
123
-
124
- // -- NAME-2 ----------------------------------------------------------------------------------------
125
-
126
- /** A name says what a thing is, never which schema generation it was built for. */
127
- export const noVersionInName = {
128
- meta: {
129
- type: "problem",
130
- docs: { description: "Identifiers do not encode a schema version." },
131
- schema: [],
132
- messages: {
133
- versioned:
134
- "`{{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`.",
135
- },
136
- },
137
- create(context) {
138
- const report = (node, name) => {
139
- if (!name || !VERSIONED_NAME.test(name)) return
140
- context.report({ node, messageId: "versioned", data: { name } })
141
- }
142
- return {
143
- FunctionDeclaration(node) {
144
- if (node.id) report(node.id, node.id.name)
145
- },
146
- ClassDeclaration(node) {
147
- if (node.id) report(node.id, node.id.name)
148
- },
149
- TSInterfaceDeclaration(node) {
150
- if (node.id) report(node.id, node.id.name)
151
- },
152
- TSTypeAliasDeclaration(node) {
153
- if (node.id) report(node.id, node.id.name)
154
- },
155
- MethodDefinition(node) {
156
- if (node.key && node.key.type === "Identifier") report(node.key, node.key.name)
157
- },
158
- }
159
- },
160
- }
161
-
162
- // -- NAME-5 ----------------------------------------------------------------------------------------
163
-
164
- /** An exported function is a verb plus its object. */
165
- export const noBareVerbExport = {
166
- meta: {
167
- type: "problem",
168
- docs: { description: "An exported function names its object, not just its verb." },
169
- schema: [],
170
- messages: {
171
- bareVerb:
172
- "`{{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.",
173
- },
174
- },
175
- create(context) {
176
- const check = (node, name) => {
177
- if (!name || !BARE_VERBS.has(name)) return
178
- context.report({ node, messageId: "bareVerb", data: { name } })
179
- }
180
- return {
181
- ExportNamedDeclaration(node) {
182
- const declaration = node.declaration
183
- if (!declaration) return
184
- if (declaration.type === "FunctionDeclaration" && declaration.id) {
185
- check(declaration.id, declaration.id.name)
186
- return
187
- }
188
- if (declaration.type !== "VariableDeclaration") return
189
- for (const one of declaration.declarations) {
190
- const init = one.init
191
- const isFunction = init
192
- && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")
193
- if (isFunction && one.id && one.id.type === "Identifier") check(one.id, one.id.name)
194
- }
195
- },
196
- }
197
- },
198
- }
199
-
200
- /** The rules this law contributes to the plugin. */
201
- export const rules = {
202
- "no-version-in-name": noVersionInName,
203
- "no-bare-verb-export": noBareVerbExport,
204
- "no-vendor-module-factory-name": noVendorModuleFactoryName,
205
- }
206
-
207
- /**
208
- * The level this law asks for, as the plugin's own opinion.
209
- *
210
- * MEASURE THESE BEFORE SWITCHING THEM ON. Unlike the other backend laws, naming rules land on a
211
- * mature tree with real debt: a repository that has been running for a while will have file names
212
- * that drifted from their exports and methods carrying a schema generation. Land above zero at
213
- * `warn` with the count and burn it down -- a naming rule at `error` on day one blocks every commit
214
- * that touches an old file, which teaches people to disable it.
215
- */
216
- export const recommended = {
217
- "starci-be/no-version-in-name": "warn",
218
- "starci-be/no-bare-verb-export": "warn",
219
- "starci-be/no-vendor-module-factory-name": "warn",
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
+ }