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