@starci/eslint-canon-fe 1.0.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/comments.mjs ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The rules that hold `comments.md`.
3
+ *
4
+ * All three reach further than their names suggest, and deliberately. A language rule that read
5
+ * only comments would leave the same sentence legal one line lower as an identifier or a diagnostic
6
+ * string; an emoji rule that read only JSX would miss the one in a log message. So each walks
7
+ * comments, identifiers, string literals, template chunks and JSX text alike - everywhere prose can
8
+ * hide in a source file.
9
+ *
10
+ * The exceptions are paths, not judgements, and that is on purpose: a locale dictionary IS the other
11
+ * language, and a fixture reproducing a real string has to reproduce it exactly. A judgement-based
12
+ * exception would be argued per file forever.
13
+ */
14
+
15
+ /** Forward-slash form of a filename, so Windows paths compare like every other path. */
16
+ const normalizePath = (filename) => String(filename || "").replace(/\\/g, "/")
17
+
18
+ /** The letters that mark the second language this source is kept free of. */
19
+ export const SECOND_LANGUAGE_LETTER = /[À-ÃÈ-ÊÌÍÒ-ÕÙÚÝà-ãè-êìíò-õùúýĂăĐđĨĩŨũƠơƯưẠ-ỿ]/
20
+
21
+ /** The endonym a language picker must be able to render in its own script. */
22
+ const ENDONYM = /Tiếng Việt/
23
+
24
+ /** A functional literal, marked with the reason it stays. */
25
+ const OK_PRAGMA = /\bvn-ok:/
26
+
27
+ /**
28
+ * Extended pictographs, or a regional-indicator pair.
29
+ *
30
+ * Two tests rather than one character class, so the class stays free of joiners and combining marks
31
+ * - a single class covering both trips the misleading-character-class rule, and a rule that has to
32
+ * be silenced to exist is one nobody trusts.
33
+ */
34
+ export const hasEmoji = (text) =>
35
+ typeof text === "string" &&
36
+ (/\p{Extended_Pictographic}/u.test(text) || /[\u{1F1E6}-\u{1F1FF}]{2}/u.test(text))
37
+
38
+ /**
39
+ * Paths whose second-language text or emoji is CONTENT rather than authoring.
40
+ *
41
+ * Two kinds only: the locale dictionaries, which are the product's other language, and fixtures,
42
+ * which reproduce real strings and would be testing something else if they translated them.
43
+ */
44
+ export const CONTENT_PATHS = [
45
+ /\/messages\/[a-z-]+\.json$/i,
46
+ /\/src\/resources\//,
47
+ /\/__fixtures?__\//,
48
+ /\/fixtures?\//,
49
+ /\.fixture\.(ts|tsx|js|mjs|cjs)$/i,
50
+ /\.test\.(ts|tsx|js|mjs|cjs)$/i,
51
+ /\.spec\.(ts|tsx|js|mjs|cjs)$/i,
52
+ ]
53
+
54
+ /** True when a file holds content rather than authoring. */
55
+ export const isContentFile = (filename) => {
56
+ const file = normalizePath(filename)
57
+ return CONTENT_PATHS.some((pattern) => pattern.test(file))
58
+ }
59
+
60
+ /** Walk every place prose can hide, and hand each to one check. */
61
+ const proseVisitors = (context, report) => {
62
+ const source = context.sourceCode || context.getSourceCode()
63
+ return {
64
+ Program() {
65
+ for (const comment of source.getAllComments()) report(comment, comment.value)
66
+ },
67
+ Identifier(node) {
68
+ report(node, node.name)
69
+ },
70
+ Literal(node) {
71
+ if (typeof node.value === "string") report(node, node.value)
72
+ },
73
+ TemplateElement(node) {
74
+ report(node, node.value && node.value.cooked)
75
+ },
76
+ JSXText(node) {
77
+ report(node, node.value)
78
+ },
79
+ }
80
+ }
81
+
82
+ // -- COMMENTS-1 ------------------------------------------------------------------------------------
83
+
84
+ /** Every export opens with a documentation block. */
85
+ export const requireExportJsdoc = {
86
+ meta: {
87
+ type: "suggestion",
88
+ docs: { description: "Exported declarations open with a documentation block." },
89
+ schema: [],
90
+ messages: {
91
+ jsdoc:
92
+ "`{{name}}` is exported with no documentation block. An export is read far more often than it is written, and by people who never open the body - name the ROLE it plays, not the signature, which the signature already states.",
93
+ },
94
+ },
95
+ create(context) {
96
+ const source = context.sourceCode || context.getSourceCode()
97
+ const hasBlock = (node) =>
98
+ source.getCommentsBefore(node).some((comment) => comment.type === "Block" && comment.value.startsWith("*"))
99
+ const check = (node) => {
100
+ const declaration = node.declaration
101
+ if (!declaration) return
102
+ const kinds = ["VariableDeclaration", "TSInterfaceDeclaration", "FunctionDeclaration", "TSTypeAliasDeclaration"]
103
+ if (!kinds.includes(declaration.type) || hasBlock(node)) return
104
+ const id =
105
+ declaration.id || (declaration.declarations && declaration.declarations[0] && declaration.declarations[0].id)
106
+ context.report({ node: id || declaration, messageId: "jsdoc", data: { name: (id && id.name) || "this export" } })
107
+ }
108
+ return { ExportNamedDeclaration: check, ExportDefaultDeclaration: check }
109
+ },
110
+ }
111
+
112
+ // -- COMMENTS-2 · COMMENTS-3 -----------------------------------------------------------------------
113
+
114
+ /** The source is English, and the exceptions are paths plus one marked pragma. */
115
+ export const noSecondLanguageInSource = {
116
+ meta: {
117
+ type: "problem",
118
+ docs: { description: "Source authoring is English; locale content and marked literals are exempt." },
119
+ schema: [],
120
+ messages: {
121
+ second:
122
+ "Second-language prose in source authoring. The bar is a stranger who joins in a year and does not share the first language of whoever wrote this line - a codebase with two languages has two populations of readers, and the smaller one stops reading the parts it cannot. If this is a literal the running program matches on or emits, keep it and mark the line with `vn-ok: <reason>`.",
123
+ },
124
+ },
125
+ create(context) {
126
+ if (isContentFile(context.filename || context.getFilename())) return {}
127
+ const source = context.sourceCode || context.getSourceCode()
128
+ /*
129
+ * Lines carrying the pragma, gathered once.
130
+ *
131
+ * The message tells the author to "mark the LINE", and until this existed
132
+ * that was not true of a literal: the exemption tested the node's OWN text,
133
+ * which for a string is its value, so `"Học viện Mộc" // vn-ok: ...` still
134
+ * reported and no phrasing could satisfy it. The rule's own valid fixture
135
+ * only passed because its string carried no diacritics at all.
136
+ *
137
+ * A marked line exempts the node ON it and the statement BELOW it, which are
138
+ * the two placements the message's wording admits.
139
+ */
140
+ const marked = new Set()
141
+ for (const comment of source.getAllComments()) {
142
+ if (!OK_PRAGMA.test(comment.value)) continue
143
+ marked.add(comment.loc.start.line)
144
+ marked.add(comment.loc.end.line + 1)
145
+ }
146
+ return proseVisitors(context, (node, text) => {
147
+ if (!text || !SECOND_LANGUAGE_LETTER.test(text)) return
148
+ if (ENDONYM.test(text) || OK_PRAGMA.test(text)) return
149
+ if (marked.has(node.loc.start.line)) return
150
+ context.report({ node, messageId: "second" })
151
+ })
152
+ },
153
+ }
154
+
155
+ // -- COMMENTS-4 ------------------------------------------------------------------------------------
156
+
157
+ /** No Unicode emoji in source: stable product reactions use attributed checked-in SVG artwork. */
158
+ export const noEmojiInSource = {
159
+ meta: {
160
+ type: "problem",
161
+ docs: { description: "No emoji in source authoring." },
162
+ schema: [],
163
+ messages: {
164
+ emoji:
165
+ "Unicode emoji in source authoring. It renders differently on every platform, sorts unpredictably, breaks a terminal that is not expecting it, and does not mean the same thing in two countries. Generic marks belong to the icon vocabulary; product reactions use the attributed checked-in SVG artwork through the reaction leaf.",
166
+ },
167
+ },
168
+ create(context) {
169
+ if (isContentFile(context.filename || context.getFilename())) return {}
170
+ return proseVisitors(context, (node, text) => {
171
+ if (hasEmoji(text)) context.report({ node, messageId: "emoji" })
172
+ })
173
+ },
174
+ }
175
+
176
+ /** The rules this law contributes to the plugin. */
177
+ export const rules = {
178
+ "require-export-jsdoc": requireExportJsdoc,
179
+ "no-second-language-in-source": noSecondLanguageInSource,
180
+ "no-emoji-in-source": noEmojiInSource,
181
+ }
182
+
183
+ /**
184
+ * The level this law asks for, as the plugin's own opinion.
185
+ *
186
+ * `require-export-jsdoc` is the one a repository adopting this with history should expect a count
187
+ * from - every undocumented export in the tree reports at once. The other two are usually already
188
+ * at zero, because a second language in source is noticed by readers long before a rule arrives.
189
+ */
190
+ export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))