@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/comments.mjs
CHANGED
|
@@ -1,389 +1,388 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold `comments.md`.
|
|
3
|
-
*
|
|
4
|
-
* Five rules, and each is careful about a different false positive:
|
|
5
|
-
*
|
|
6
|
-
* - `require-export-jsdoc` skips plain data constants. `export const MAX_ATTEMPTS = 3` is already
|
|
7
|
-
* fully described by its own name, and demanding a sentence there produces sentences that
|
|
8
|
-
* restate the name - which COMMENT-3 forbids. Only declarations with a surface get the check.
|
|
9
|
-
* - `require-enum-member-jsdoc` can check that a doc EXISTS and never that it states a
|
|
10
|
-
* consequence. That half is read by a person, and the rule says so rather than pretending.
|
|
11
|
-
* - `no-non-ascii-source` exempts a marked line, because text a program matches on or emits is
|
|
12
|
-
* data wearing prose, and translating it breaks the program.
|
|
13
|
-
* - `require-vn-ok-reason` (law 6) holds the half `no-non-ascii-source` cannot: that marker is a
|
|
14
|
-
* bare escape hatch unless it says why the line stays, so a bare `vn-ok` is treated the same as
|
|
15
|
-
* no marker at all.
|
|
16
|
-
* - `no-restated-name-jsdoc` (law 7) holds the decidable slice of law 3 - a doc block whose only
|
|
17
|
-
* content is the declared name re-spelled in words teaches nothing beyond the import line, so it
|
|
18
|
-
* is COMMENT-3's violation wearing COMMENT-1's shape. It fires only on an exact match between the
|
|
19
|
-
* doc's content words and the name's own words, so a doc that adds any real information is left
|
|
20
|
-
* alone - the rest of law 3 (whether a sentence explains something outside the line) is not
|
|
21
|
-
* decidable and stays a human read.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
if (
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const
|
|
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
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.replace(/
|
|
278
|
-
.
|
|
279
|
-
.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
export
|
|
368
|
-
"require-
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
export
|
|
384
|
-
"starci-be/require-
|
|
385
|
-
"starci-be/
|
|
386
|
-
"starci-be/
|
|
387
|
-
"starci-be/
|
|
388
|
-
|
|
389
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `comments.md`.
|
|
3
|
+
*
|
|
4
|
+
* Five rules, and each is careful about a different false positive:
|
|
5
|
+
*
|
|
6
|
+
* - `require-export-jsdoc` skips plain data constants. `export const MAX_ATTEMPTS = 3` is already
|
|
7
|
+
* fully described by its own name, and demanding a sentence there produces sentences that
|
|
8
|
+
* restate the name - which COMMENT-3 forbids. Only declarations with a surface get the check.
|
|
9
|
+
* - `require-enum-member-jsdoc` can check that a doc EXISTS and never that it states a
|
|
10
|
+
* consequence. That half is read by a person, and the rule says so rather than pretending.
|
|
11
|
+
* - `no-non-ascii-source` exempts a marked line, because text a program matches on or emits is
|
|
12
|
+
* data wearing prose, and translating it breaks the program.
|
|
13
|
+
* - `require-vn-ok-reason` (law 6) holds the half `no-non-ascii-source` cannot: that marker is a
|
|
14
|
+
* bare escape hatch unless it says why the line stays, so a bare `vn-ok` is treated the same as
|
|
15
|
+
* no marker at all.
|
|
16
|
+
* - `no-restated-name-jsdoc` (law 7) holds the decidable slice of law 3 - a doc block whose only
|
|
17
|
+
* content is the declared name re-spelled in words teaches nothing beyond the import line, so it
|
|
18
|
+
* is COMMENT-3's violation wearing COMMENT-1's shape. It fires only on an exact match between the
|
|
19
|
+
* doc's content words and the name's own words, so a doc that adds any real information is left
|
|
20
|
+
* alone - the rest of law 3 (whether a sentence explains something outside the line) is not
|
|
21
|
+
* decidable and stays a human read.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { normalizePath } from "./lib/path.mjs"
|
|
25
|
+
|
|
26
|
+
/** The marker that keeps a non-ASCII literal the program depends on. */
|
|
27
|
+
const KEEP_MARKER = /\bvn-ok\b/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The character classes this refuses, and why it is NOT simply "ASCII only".
|
|
31
|
+
*
|
|
32
|
+
* A first cut of this rule banned every non-ASCII codepoint. Measured against the reference
|
|
33
|
+
* repository it reported 857 offenders -- and every one was an em dash, a box-drawing run in a
|
|
34
|
+
* comment banner, or a middle dot. The code has used those freely and deliberately for its whole
|
|
35
|
+
* life, so "ASCII only" was not the law being recorded; it was a stricter law being invented, which
|
|
36
|
+
* is the one thing canon must not do.
|
|
37
|
+
*
|
|
38
|
+
* What the law actually refuses is three things, for three different reasons:
|
|
39
|
+
*
|
|
40
|
+
* - VIETNAMESE letters, because a reader who does not share the author's first language loses
|
|
41
|
+
* exactly the half of the reasoning that explains the surprising parts. Matched precisely, so a
|
|
42
|
+
* European loanword (`naive`, `facade`, `Muller`) is never a false positive.
|
|
43
|
+
* - EMOJI, because they carry tone rather than information, and tone reads differently to
|
|
44
|
+
* everybody.
|
|
45
|
+
* - DECORATIVE symbols -- check marks, crosses, arrows used as ornament -- for the same reason.
|
|
46
|
+
*
|
|
47
|
+
* Typographic punctuation is none of those and stays.
|
|
48
|
+
*/
|
|
49
|
+
const VIETNAMESE_LETTER = /[À-ÃÈ-ÊÌÍÒ-ÕÙÚÝà-ãè-êìíò-õùúýĂăĐđĨĩŨũƠơƯưẠ-ỿ]/
|
|
50
|
+
|
|
51
|
+
/** Emoji and pictographs. */
|
|
52
|
+
const EMOJI = /[\u{1F300}-\u{1FAFF}\u{1F000}-\u{1F0FF}\u{2600}-\u{27BF}\u{FE0F}\u{1F1E6}-\u{1F1FF}]/u
|
|
53
|
+
|
|
54
|
+
/** Ornamental marks that stand in for a word. */
|
|
55
|
+
const ORNAMENT = /[✅❌✔✖✗✘⭐⬆⬇➡⬅]/
|
|
56
|
+
|
|
57
|
+
/** The Vietnamese language's own name, which is a label rather than prose. */
|
|
58
|
+
const ENDONYM = /Tiếng Việt/
|
|
59
|
+
|
|
60
|
+
/** Declarations whose surface other files depend on. */
|
|
61
|
+
const DOCUMENTED_KINDS = new Set([
|
|
62
|
+
"TSInterfaceDeclaration",
|
|
63
|
+
"TSTypeAliasDeclaration",
|
|
64
|
+
"TSEnumDeclaration",
|
|
65
|
+
"ClassDeclaration",
|
|
66
|
+
"FunctionDeclaration",
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
/** What a line offends with, or null. */
|
|
70
|
+
const offenceIn = (line) => {
|
|
71
|
+
const withoutEndonym = line.replace(ENDONYM, "")
|
|
72
|
+
if (VIETNAMESE_LETTER.test(withoutEndonym)) return "a Vietnamese letter"
|
|
73
|
+
if (EMOJI.test(withoutEndonym)) return "an emoji"
|
|
74
|
+
if (ORNAMENT.test(withoutEndonym)) return "an ornamental symbol"
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether a JSDoc block sits immediately before a node. */
|
|
79
|
+
const hasJsdocBefore = (sourceCode, node) =>
|
|
80
|
+
sourceCode
|
|
81
|
+
.getCommentsBefore(node)
|
|
82
|
+
.some((comment) => comment.type === "Block" && comment.value.startsWith("*"))
|
|
83
|
+
|
|
84
|
+
/** The declared name, for the message. */
|
|
85
|
+
const nameOf = (declaration) => {
|
|
86
|
+
if (declaration.id) return declaration.id.name
|
|
87
|
+
const first = declaration.declarations && declaration.declarations[0]
|
|
88
|
+
return (first && first.id && first.id.name) || "this export"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// -- COMMENT-1 -------------------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/** Every export with a surface opens with a doc block. */
|
|
94
|
+
export const requireExportJsdoc = {
|
|
95
|
+
meta: {
|
|
96
|
+
type: "suggestion",
|
|
97
|
+
docs: { description: "An exported class/interface/type/enum/function opens with JSDoc." },
|
|
98
|
+
schema: [],
|
|
99
|
+
messages: {
|
|
100
|
+
jsdoc:
|
|
101
|
+
"`{{name}}` is exported with no doc block. This is surface other files depend on, and a name plus a signature says what it TAKES - never what it is for, or when to reach for it rather than the thing beside it.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
create(context) {
|
|
105
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
106
|
+
const check = (node) => {
|
|
107
|
+
const declaration = node.declaration
|
|
108
|
+
// a re-export has nothing here to attach a doc to
|
|
109
|
+
if (!declaration) return
|
|
110
|
+
if (declaration.type === "VariableDeclaration") {
|
|
111
|
+
// only a const bound to a function has a surface; a data constant is already described
|
|
112
|
+
const first = declaration.declarations[0]
|
|
113
|
+
const init = first && first.init
|
|
114
|
+
const isFunction = init
|
|
115
|
+
&& (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")
|
|
116
|
+
if (!isFunction) return
|
|
117
|
+
} else if (!DOCUMENTED_KINDS.has(declaration.type)) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (hasJsdocBefore(sourceCode, node)) return
|
|
121
|
+
context.report({
|
|
122
|
+
node: declaration.id || declaration,
|
|
123
|
+
messageId: "jsdoc",
|
|
124
|
+
data: { name: nameOf(declaration) },
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ExportNamedDeclaration: check,
|
|
129
|
+
ExportDefaultDeclaration: check,
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// -- COMMENT-2 -------------------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/** Every member of an exported enum carries its own doc. */
|
|
137
|
+
export const requireEnumMemberJsdoc = {
|
|
138
|
+
meta: {
|
|
139
|
+
type: "suggestion",
|
|
140
|
+
docs: { description: "Every member of an exported enum carries its own JSDoc." },
|
|
141
|
+
schema: [],
|
|
142
|
+
messages: {
|
|
143
|
+
jsdoc:
|
|
144
|
+
"Enum member `{{name}}` has no doc. State what CHOOSING it causes, not what it is called - a member is picked at a call site far from the switch that gives it meaning. (A rule can only see that a doc exists; whether it states a consequence is read by a person.)",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
create(context) {
|
|
148
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
149
|
+
return {
|
|
150
|
+
TSEnumDeclaration(node) {
|
|
151
|
+
if (!node.parent || node.parent.type !== "ExportNamedDeclaration") return
|
|
152
|
+
for (const member of node.members || []) {
|
|
153
|
+
if (hasJsdocBefore(sourceCode, member)) continue
|
|
154
|
+
const name = member.id && (member.id.name || member.id.value)
|
|
155
|
+
context.report({ node: member, messageId: "jsdoc", data: { name: name || "?" } })
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// -- COMMENT-4 -------------------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/** Source is English ASCII, except where a literal is data the program depends on. */
|
|
165
|
+
export const noNonAsciiSource = {
|
|
166
|
+
meta: {
|
|
167
|
+
type: "problem",
|
|
168
|
+
docs: { description: "Source stays English ASCII; a depended-upon literal is marked instead." },
|
|
169
|
+
schema: [],
|
|
170
|
+
messages: {
|
|
171
|
+
nonAscii:
|
|
172
|
+
"This line carries {{offence}}. The bar is a reader who does not share the author's first language: a codebase with two languages in it has somebody for whom half the reasoning is unavailable, and it is the half explaining the surprising parts - and an emoji or an ornament carries tone rather than information, which reads differently to everybody. If this is text the program MATCHES on or EMITS, it is data rather than prose: keep it and mark the line `vn-ok: <reason>` so the next sweep does not turn it into a bug.",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
create(context) {
|
|
176
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
177
|
+
const file = normalizePath(context.filename || context.getFilename())
|
|
178
|
+
// locale files are wholly product copy; policing them would be policing the product
|
|
179
|
+
if (/\/(?:messages|locales|i18n)\//.test(file)) return {}
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* IN A TEST LANE, A STRING IS A FIXTURE AND A COMMENT IS STILL PROSE.
|
|
183
|
+
*
|
|
184
|
+
* The rule's own reason draws this line already: prose in a second language leaves half the
|
|
185
|
+
* reasoning unavailable to a reader, while "text the program MATCHES on or EMITS is data rather
|
|
186
|
+
* than prose". A spec that feeds an agent the sentence a real customer would type - "khach vua
|
|
187
|
+
* chuyen khoan" - is feeding it data, and translating it would test a system nobody uses.
|
|
188
|
+
*
|
|
189
|
+
* Measured before it was written: of 92 findings in one back end, 89 were fixture strings and 3
|
|
190
|
+
* were comments. Demanding `vn-ok` on all 92 would have put a marker on every line of every
|
|
191
|
+
* conversation fixture, which is noise that teaches a reader to stop seeing the marker.
|
|
192
|
+
*
|
|
193
|
+
* The exemption is for STRINGS ONLY. A Vietnamese comment in a spec is the same problem it is
|
|
194
|
+
* anywhere else - the next reader still cannot follow the reasoning - so it is still refused.
|
|
195
|
+
*/
|
|
196
|
+
const fixtureLane = /\.spec\.ts$|-spec\.ts$|\/src\/tests\//.test(file)
|
|
197
|
+
const commentLines = fixtureLane
|
|
198
|
+
? new Set(sourceCode.getAllComments().flatMap((comment) => {
|
|
199
|
+
const span = []
|
|
200
|
+
for (let line = comment.loc.start.line; line <= comment.loc.end.line; line += 1) span.push(line)
|
|
201
|
+
return span
|
|
202
|
+
}))
|
|
203
|
+
: null
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"Program:exit"(node) {
|
|
207
|
+
const lines = sourceCode.getLines()
|
|
208
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
209
|
+
const line = lines[index]
|
|
210
|
+
if (KEEP_MARKER.test(line)) continue
|
|
211
|
+
// in a fixture lane only prose is policed; the data the fixture feeds is the point of it
|
|
212
|
+
if (commentLines && !commentLines.has(index + 1)) continue
|
|
213
|
+
const offence = offenceIn(line)
|
|
214
|
+
if (offence === null) continue
|
|
215
|
+
context.report({
|
|
216
|
+
node,
|
|
217
|
+
loc: {
|
|
218
|
+
line: index + 1,
|
|
219
|
+
column: 0,
|
|
220
|
+
},
|
|
221
|
+
messageId: "nonAscii",
|
|
222
|
+
data: {
|
|
223
|
+
offence,
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// -- law 6 -------------------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
/** The line-level test for law 6: `vn-ok` present, with a colon and at least one reason character after it. */
|
|
235
|
+
const REASON_AFTER_MARKER = /\bvn-ok\s*:\s*\S/
|
|
236
|
+
|
|
237
|
+
/** Every `vn-ok` marker carries a reason; a bare marker is not an exemption. */
|
|
238
|
+
export const requireVnOkReason = {
|
|
239
|
+
meta: {
|
|
240
|
+
type: "problem",
|
|
241
|
+
docs: {
|
|
242
|
+
description:
|
|
243
|
+
"COMMENT-5, law 6: a `vn-ok` marker carries a reason - a bare marker is not an exemption.",
|
|
244
|
+
},
|
|
245
|
+
schema: [],
|
|
246
|
+
messages: {
|
|
247
|
+
bareMarker:
|
|
248
|
+
"`vn-ok` on this line has no reason after it. The marker exists so the next sweep can read WHY the line stays instead of \"fixing\" it into a bug - a bare marker is only a way of switching that check off. Write `vn-ok: <reason>` and say what the marker is protecting.",
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
create(context) {
|
|
252
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
253
|
+
return {
|
|
254
|
+
"Program:exit"(node) {
|
|
255
|
+
const lines = sourceCode.getLines()
|
|
256
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
257
|
+
const line = lines[index]
|
|
258
|
+
if (!KEEP_MARKER.test(line)) continue
|
|
259
|
+
if (REASON_AFTER_MARKER.test(line)) continue
|
|
260
|
+
context.report({
|
|
261
|
+
node,
|
|
262
|
+
loc: { line: index + 1, column: 0 },
|
|
263
|
+
messageId: "bareMarker",
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// -- law 7 -------------------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
/** Splits a declared name into its lowercase words, so `readUser` compares against "read user". */
|
|
274
|
+
const wordsOf = (name) =>
|
|
275
|
+
(String(name || "")
|
|
276
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
277
|
+
.replace(/[_-]+/g, " ")
|
|
278
|
+
.toLowerCase()
|
|
279
|
+
.match(/[a-z0-9]+/g) || [])
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Filler a doc contributes no information by using: articles and linking words, plus the two generic
|
|
283
|
+
* nouns the law's own anchor examples restate with ("the pending state", "the read user function").
|
|
284
|
+
* Nothing here strips a word that could carry real content, so a doc that explains anything beyond
|
|
285
|
+
* the name keeps enough leftover words to fail the equality check below.
|
|
286
|
+
*/
|
|
287
|
+
const RESTATEMENT_FILLER = new Set(["a", "an", "the", "is", "of", "for", "to", "this", "that", "function", "state"])
|
|
288
|
+
|
|
289
|
+
/** The content words a doc comment contributes once filler is stripped out. */
|
|
290
|
+
const docContentWords = (comment) =>
|
|
291
|
+
(comment.value.match(/[A-Za-z]+/g) || [])
|
|
292
|
+
.map((word) => word.toLowerCase())
|
|
293
|
+
.filter((word) => !RESTATEMENT_FILLER.has(word))
|
|
294
|
+
|
|
295
|
+
/** Whether a doc's leftover content words are exactly the declared name's own words - nothing added. */
|
|
296
|
+
const isPureRestatement = (comment, declaredName) => {
|
|
297
|
+
const content = docContentWords(comment)
|
|
298
|
+
const identifier = wordsOf(declaredName)
|
|
299
|
+
if (content.length === 0 || identifier.length === 0) return false
|
|
300
|
+
const contentSet = new Set(content)
|
|
301
|
+
const identifierSet = new Set(identifier)
|
|
302
|
+
if (contentSet.size !== identifierSet.size) return false
|
|
303
|
+
for (const word of contentSet) if (!identifierSet.has(word)) return false
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The doc block immediately before a node, or null. */
|
|
308
|
+
const jsdocBefore = (sourceCode, node) =>
|
|
309
|
+
sourceCode.getCommentsBefore(node).find((comment) => comment.type === "Block" && comment.value.startsWith("*"))
|
|
310
|
+
|| null
|
|
311
|
+
|
|
312
|
+
/** A doc block that only re-spells the declared name is COMMENT-3's violation wearing COMMENT-1's shape. */
|
|
313
|
+
export const noRestatedNameJsdoc = {
|
|
314
|
+
meta: {
|
|
315
|
+
type: "suggestion",
|
|
316
|
+
docs: {
|
|
317
|
+
description:
|
|
318
|
+
"COMMENT-3, law 7: a doc block that restates the declared name is a COMMENT-3 violation wearing a COMMENT-1 shape, even while the lint gate that checks a doc EXISTS stays green.",
|
|
319
|
+
},
|
|
320
|
+
schema: [],
|
|
321
|
+
messages: {
|
|
322
|
+
restated:
|
|
323
|
+
"This doc block only re-spells `{{name}}` in words. A name plus a signature already says what it TAKES; the doc has to say what it is FOR, or what choosing it causes, or it is a COMMENT-3 violation wearing a COMMENT-1 shape. Delete the restatement and write the reason instead.",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
create(context) {
|
|
327
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
328
|
+
const checkExport = (node) => {
|
|
329
|
+
const declaration = node.declaration
|
|
330
|
+
// a re-export has nothing here to attach a doc to
|
|
331
|
+
if (!declaration) return
|
|
332
|
+
if (declaration.type === "VariableDeclaration") {
|
|
333
|
+
// only a const bound to a function has a surface; a data constant is already described
|
|
334
|
+
const first = declaration.declarations[0]
|
|
335
|
+
const init = first && first.init
|
|
336
|
+
const isFunction = init
|
|
337
|
+
&& (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")
|
|
338
|
+
if (!isFunction) return
|
|
339
|
+
} else if (!DOCUMENTED_KINDS.has(declaration.type)) {
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
const doc = jsdocBefore(sourceCode, node)
|
|
343
|
+
if (!doc) return
|
|
344
|
+
const name = nameOf(declaration)
|
|
345
|
+
if (!isPureRestatement(doc, name)) return
|
|
346
|
+
context.report({ node: declaration.id || declaration, messageId: "restated", data: { name } })
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
ExportNamedDeclaration: checkExport,
|
|
350
|
+
ExportDefaultDeclaration: checkExport,
|
|
351
|
+
TSEnumDeclaration(node) {
|
|
352
|
+
if (!node.parent || node.parent.type !== "ExportNamedDeclaration") return
|
|
353
|
+
for (const member of node.members || []) {
|
|
354
|
+
const doc = jsdocBefore(sourceCode, member)
|
|
355
|
+
if (!doc) continue
|
|
356
|
+
const name = member.id && (member.id.name || member.id.value)
|
|
357
|
+
if (!name || !isPureRestatement(doc, name)) continue
|
|
358
|
+
context.report({ node: member, messageId: "restated", data: { name } })
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The rules this law contributes to the plugin. */
|
|
366
|
+
export const rules = {
|
|
367
|
+
"require-export-jsdoc": requireExportJsdoc,
|
|
368
|
+
"require-enum-member-jsdoc": requireEnumMemberJsdoc,
|
|
369
|
+
"no-non-ascii-source": noNonAsciiSource,
|
|
370
|
+
"require-vn-ok-reason": requireVnOkReason,
|
|
371
|
+
"no-restated-name-jsdoc": noRestatedNameJsdoc,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The level this law asks for, as the plugin's own opinion.
|
|
376
|
+
*
|
|
377
|
+
* `no-non-ascii-source` replaces three separate rules in the reference plugin - one for Vietnamese,
|
|
378
|
+
* one for emoji, one for decorative symbols. They were three character classes answering one
|
|
379
|
+
* question, and a reader who hit the emoji rule learned nothing about the other two. One rule with
|
|
380
|
+
* one reason is easier to obey, and impossible to satisfy by switching alphabets.
|
|
381
|
+
*/
|
|
382
|
+
export const recommended = {
|
|
383
|
+
"starci-be/require-export-jsdoc": "error",
|
|
384
|
+
"starci-be/require-enum-member-jsdoc": "error",
|
|
385
|
+
"starci-be/no-non-ascii-source": "error",
|
|
386
|
+
"starci-be/require-vn-ok-reason": "error",
|
|
387
|
+
"starci-be/no-restated-name-jsdoc": "error",
|
|
388
|
+
}
|