@blumintinc/eslint-plugin-blumint 1.16.2 → 1.17.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.
Files changed (49) hide show
  1. package/README.md +21 -0
  2. package/lib/index.js +64 -1
  3. package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
  4. package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
  5. package/lib/rules/enforce-cloud-function-id-length.js +104 -0
  6. package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
  7. package/lib/rules/enforce-is-prefix-validators.js +304 -0
  8. package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
  9. package/lib/rules/enforce-m3-sentence-case.js +430 -0
  10. package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
  11. package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
  12. package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
  13. package/lib/rules/enforce-types-directory-placement.js +276 -0
  14. package/lib/rules/no-direct-function-state.d.ts +8 -0
  15. package/lib/rules/no-direct-function-state.js +285 -0
  16. package/lib/rules/no-fill-template-mutation.d.ts +1 -0
  17. package/lib/rules/no-fill-template-mutation.js +324 -0
  18. package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
  19. package/lib/rules/no-portal-inside-tooltip.js +219 -0
  20. package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
  21. package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
  22. package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
  23. package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
  24. package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
  25. package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
  26. package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
  27. package/lib/rules/no-stablehash-react-nodes.js +325 -0
  28. package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
  29. package/lib/rules/parallelize-loop-awaits.js +582 -0
  30. package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
  31. package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
  32. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
  33. package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
  34. package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
  35. package/lib/rules/prefer-spread-over-reassembly.js +401 -0
  36. package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
  37. package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
  38. package/lib/rules/prefer-use-base62-id.d.ts +8 -0
  39. package/lib/rules/prefer-use-base62-id.js +483 -0
  40. package/lib/rules/prefer-use-theme.d.ts +1 -0
  41. package/lib/rules/prefer-use-theme.js +206 -0
  42. package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
  43. package/lib/rules/prefer-utility-function-own-file.js +505 -0
  44. package/lib/rules/require-props-composition.d.ts +10 -0
  45. package/lib/rules/require-props-composition.js +433 -0
  46. package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
  47. package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
  48. package/package.json +1 -1
  49. package/release-manifest.json +190 -0
@@ -0,0 +1,430 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceM3SentenceCase = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ /**
7
+ * Default props that carry user-facing label text, per the issue spec.
8
+ */
9
+ const DEFAULT_PROPS_TO_CHECK = new Set([
10
+ 'label',
11
+ 'title',
12
+ 'placeholder',
13
+ 'helperText',
14
+ 'message',
15
+ 'description',
16
+ 'tooltip',
17
+ 'buttonText',
18
+ 'aria-label',
19
+ 'alt',
20
+ ]);
21
+ /**
22
+ * Words that are always allowed to keep their original capitalisation.
23
+ * Populated with brand names / proper nouns from the issue spec plus a broad set
24
+ * of common platform, game, and tech names.
25
+ */
26
+ const DEFAULT_IGNORED_WORDS = new Set([
27
+ // BluMint brand
28
+ 'BluMint',
29
+ // Auth / social
30
+ 'Google',
31
+ 'Apple',
32
+ 'Facebook',
33
+ 'Discord',
34
+ 'Twitch',
35
+ 'Steam',
36
+ 'YouTube',
37
+ 'Twitter',
38
+ 'Instagram',
39
+ 'TikTok',
40
+ 'Reddit',
41
+ 'GitHub',
42
+ 'LinkedIn',
43
+ // Games / platforms
44
+ 'Overwolf',
45
+ 'Fortnite',
46
+ 'Valorant',
47
+ 'Apex',
48
+ 'Legends',
49
+ 'Rocket',
50
+ 'League',
51
+ 'Minecraft',
52
+ 'Roblox',
53
+ 'PlayStation',
54
+ 'Xbox',
55
+ 'Nintendo',
56
+ // Other tech
57
+ 'Windows',
58
+ 'macOS',
59
+ 'Linux',
60
+ 'Android',
61
+ 'iPhone',
62
+ 'iPad',
63
+ // Common acronyms treated as words (capitalised but not ALL-CAPS)
64
+ 'iOS',
65
+ 'macOS',
66
+ ]);
67
+ /**
68
+ * Short all-caps tokens that are valid acronyms and must not be flagged as
69
+ * ALL-CAPS violations. Entries are case-sensitive (already upper-case).
70
+ */
71
+ const ACRONYM_ALLOWLIST = new Set([
72
+ 'OK',
73
+ 'ID',
74
+ 'IDs',
75
+ 'API',
76
+ 'APIs',
77
+ 'URL',
78
+ 'URLs',
79
+ 'URI',
80
+ 'URIs',
81
+ 'FAQ',
82
+ 'FAQs',
83
+ 'NFT',
84
+ 'NFTs',
85
+ 'USD',
86
+ 'EUR',
87
+ 'GBP',
88
+ 'DM',
89
+ 'DMs',
90
+ 'OBS',
91
+ 'RTMP',
92
+ 'RTMPS',
93
+ 'CDN',
94
+ 'SDK',
95
+ 'UI',
96
+ 'UX',
97
+ 'AI',
98
+ 'ML',
99
+ 'VR',
100
+ 'AR',
101
+ 'PR',
102
+ 'QR',
103
+ 'vs',
104
+ 'VS',
105
+ ]);
106
+ /**
107
+ * Maximum character length for a token that is treated as a valid short acronym
108
+ * even if it is not in the explicit allowlist (e.g. three-letter country codes
109
+ * like "USA", "EUR", etc.).
110
+ */
111
+ const SHORT_ACRONYM_MAX_LENGTH = 4;
112
+ /**
113
+ * Returns true when a single whitespace-free token should be treated as a
114
+ * valid acronym / abbreviation and ignored.
115
+ */
116
+ function isAcronymToken(word) {
117
+ if (ACRONYM_ALLOWLIST.has(word))
118
+ return true;
119
+ // All-caps tokens up to the threshold length are treated as acronyms.
120
+ if (word.length <= SHORT_ACRONYM_MAX_LENGTH && /^[A-Z]+$/.test(word)) {
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+ /**
126
+ * Checks whether the raw text looks like code, a URL, or a file-path and
127
+ * should be skipped entirely.
128
+ */
129
+ function looksLikeCodeOrUrl(text) {
130
+ // URLs
131
+ if (/https?:\/\//i.test(text))
132
+ return true;
133
+ // File paths (starts with /, ./, ../)
134
+ if (/^\.{0,2}\//.test(text))
135
+ return true;
136
+ // camelCase / PascalCase single tokens with no spaces
137
+ if (/^\S+$/.test(text) && /[a-z][A-Z]/.test(text))
138
+ return true;
139
+ return false;
140
+ }
141
+ /**
142
+ * Segments a string at sentence boundaries (`.`, `?`, `!`, or `:` followed by
143
+ * whitespace) and at the colon heuristic described in the issue. Each returned
144
+ * segment starts at a sentence-boundary, so its first word is allowed to carry
145
+ * a capital letter.
146
+ */
147
+ function splitIntoSentences(text) {
148
+ // Split on `. `, `? `, `! `, `: ` — the character after the punctuation is
149
+ // trimmed so each segment's first word is examined independently.
150
+ const segments = text.split(/(?<=[.?!:])\s+/);
151
+ return segments.map((s) => s.trim()).filter((s) => s.length > 0);
152
+ }
153
+ /**
154
+ * Given a sentence (first word allowed to be capitalised), returns the words
155
+ * that carry an unexpected capital — i.e. non-first words whose first letter is
156
+ * upper-case AND which are not in the ignored set AND not acronyms.
157
+ */
158
+ function titleCaseViolatingWords(sentence, ignoredWordsSet) {
159
+ const words = sentence.split(/\s+/);
160
+ const violating = [];
161
+ words.forEach((raw, index) => {
162
+ // Strip surrounding punctuation to get the bare word for checks
163
+ const word = raw.replace(/^[^\w]+|[^\w]+$/g, '');
164
+ if (!word)
165
+ return;
166
+ // First word of the sentence — allowed to start with a capital
167
+ if (index === 0)
168
+ return;
169
+ // Ignored (proper noun / brand)
170
+ if (ignoredWordsSet.has(word))
171
+ return;
172
+ // Acronym
173
+ if (isAcronymToken(word))
174
+ return;
175
+ // Only flag if the first character is an upper-case letter
176
+ if (/^[A-Z]/.test(word)) {
177
+ violating.push(word);
178
+ }
179
+ });
180
+ return violating;
181
+ }
182
+ /**
183
+ * Returns true when the text is an ALL-CAPS violation:
184
+ * — ≥ 2 words
185
+ * — The full letter content is upper-case
186
+ * — It cannot be decomposed into individual acronyms
187
+ */
188
+ function isAllCapsViolation(text, ignoredWordsSet) {
189
+ const words = text.trim().split(/\s+/);
190
+ if (words.length < 2)
191
+ return false;
192
+ // All letter characters must be upper-case
193
+ const letters = text.replace(/[^a-zA-Z]/g, '');
194
+ if (letters !== letters.toUpperCase())
195
+ return false;
196
+ // If every word is an acronym or ignored, it is not a violation
197
+ const allExempt = words.every((w) => isAcronymToken(w) || ignoredWordsSet.has(w));
198
+ return !allExempt;
199
+ }
200
+ /**
201
+ * Builds the corrected version of the text for the suggestion / fix.
202
+ * — ALL-CAPS: Capitalises only the first letter of the first word, lowercases
203
+ * the rest (respecting acronyms and ignored words).
204
+ * — Title Case: Lowercases non-first words that are not acronyms / proper nouns.
205
+ */
206
+ function buildSuggestionText(text, ignoredWordsSet) {
207
+ const sentences = splitIntoSentences(text);
208
+ return sentences
209
+ .map((sentence) => {
210
+ const words = sentence.split(/\s+/);
211
+ return words
212
+ .map((raw, index) => {
213
+ const word = raw.replace(/^[^\w]+|[^\w]+$/g, '');
214
+ if (!word)
215
+ return raw;
216
+ if (index === 0) {
217
+ // Preserve ignored words in their original capitalisation; for all-caps
218
+ // first words, lower-case everything except the initial letter.
219
+ if (ignoredWordsSet.has(word))
220
+ return raw;
221
+ if (isAcronymToken(word))
222
+ return raw;
223
+ // Sentence-start: ensure first letter is capital
224
+ return raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase();
225
+ }
226
+ if (ignoredWordsSet.has(word))
227
+ return raw;
228
+ if (isAcronymToken(word))
229
+ return raw;
230
+ // Non-first word: lower-case
231
+ return raw.charAt(0).toLowerCase() + raw.slice(1);
232
+ })
233
+ .join(' ');
234
+ })
235
+ .join(' ');
236
+ }
237
+ /**
238
+ * Returns the trimmed text to check and whether it is worth checking.
239
+ * JSXText nodes often contain only whitespace / newlines from formatting.
240
+ */
241
+ function extractCheckableText(raw) {
242
+ const trimmed = raw.trim();
243
+ // Nothing to check
244
+ if (!trimmed)
245
+ return null;
246
+ // Numeric / symbol only — not user-facing text
247
+ if (/^[\d\W]+$/.test(trimmed))
248
+ return null;
249
+ // Code / URL patterns
250
+ if (looksLikeCodeOrUrl(trimmed))
251
+ return null;
252
+ return trimmed;
253
+ }
254
+ exports.enforceM3SentenceCase = (0, createRule_1.createRule)({
255
+ name: 'enforce-m3-sentence-case',
256
+ meta: {
257
+ type: 'suggestion',
258
+ docs: {
259
+ description: 'Enforce Material Design 3 sentence-case capitalisation for user-facing text — flag Title Case and ALL CAPS strings in JSX text and configured string props.',
260
+ recommended: 'warn',
261
+ },
262
+ hasSuggestions: true,
263
+ schema: [
264
+ {
265
+ type: 'object',
266
+ properties: {
267
+ propsToCheck: {
268
+ type: 'array',
269
+ items: { type: 'string' },
270
+ },
271
+ ignoredWords: {
272
+ type: 'array',
273
+ items: { type: 'string' },
274
+ },
275
+ ignorePatterns: {
276
+ type: 'array',
277
+ items: { type: 'string' },
278
+ },
279
+ allowList: {
280
+ type: 'array',
281
+ items: { type: 'string' },
282
+ },
283
+ checkJsxText: {
284
+ type: 'boolean',
285
+ },
286
+ },
287
+ additionalProperties: false,
288
+ },
289
+ ],
290
+ messages: {
291
+ titleCase: 'Text "{{text}}" uses Title Case. Material Design 3 requires sentence case — only the first word and proper nouns should be capitalised. Consider "{{suggestion}}".',
292
+ allCaps: 'Text "{{text}}" is ALL CAPS. Material Design 3 requires sentence case — use "{{suggestion}}" instead.',
293
+ },
294
+ },
295
+ defaultOptions: [{}],
296
+ create(context, [options]) {
297
+ const propsToCheckSet = options.propsToCheck
298
+ ? new Set(options.propsToCheck)
299
+ : DEFAULT_PROPS_TO_CHECK;
300
+ const ignoredWordsSet = new Set([
301
+ ...DEFAULT_IGNORED_WORDS,
302
+ ...(options.ignoredWords ?? []),
303
+ ]);
304
+ const ignorePatternRegexes = (options.ignorePatterns ?? []).map((p) => new RegExp(p));
305
+ const allowListSet = new Set(options.allowList ?? []);
306
+ const checkJsxText = options.checkJsxText !== false;
307
+ /**
308
+ * Determine whether the text should be skipped before casing checks.
309
+ */
310
+ function shouldSkip(text) {
311
+ if (allowListSet.has(text))
312
+ return true;
313
+ if (ignorePatternRegexes.some((re) => re.test(text)))
314
+ return true;
315
+ return false;
316
+ }
317
+ /**
318
+ * Core checker. Reports on `reportNode` if `text` violates M3 sentence case.
319
+ */
320
+ function checkText(text, reportNode) {
321
+ const checkable = extractCheckableText(text);
322
+ if (!checkable)
323
+ return;
324
+ if (shouldSkip(checkable))
325
+ return;
326
+ // ALL-CAPS check first (higher severity and different fix)
327
+ if (isAllCapsViolation(checkable, ignoredWordsSet)) {
328
+ const suggestion = buildSuggestionText(checkable, ignoredWordsSet);
329
+ context.report({
330
+ node: reportNode,
331
+ messageId: 'allCaps',
332
+ data: { text: checkable, suggestion },
333
+ suggest: [
334
+ {
335
+ messageId: 'allCaps',
336
+ data: { text: checkable, suggestion },
337
+ fix(fixer) {
338
+ if (reportNode.type === utils_1.AST_NODE_TYPES.Literal) {
339
+ const raw = context.getSourceCode().getText(reportNode);
340
+ const quote = raw[0];
341
+ return fixer.replaceText(reportNode, `${quote}${suggestion}${quote}`);
342
+ }
343
+ if (reportNode.type === utils_1.AST_NODE_TYPES.JSXText) {
344
+ const original = reportNode.value;
345
+ const replaced = original.replace(checkable, suggestion);
346
+ return fixer.replaceText(reportNode, replaced);
347
+ }
348
+ return null;
349
+ },
350
+ },
351
+ ],
352
+ });
353
+ return;
354
+ }
355
+ // Title-Case check: validate each sentence segment independently
356
+ const sentences = splitIntoSentences(checkable);
357
+ const violatingWords = [];
358
+ sentences.forEach((sentence) => {
359
+ violatingWords.push(...titleCaseViolatingWords(sentence, ignoredWordsSet));
360
+ });
361
+ if (violatingWords.length > 0) {
362
+ const suggestion = buildSuggestionText(checkable, ignoredWordsSet);
363
+ context.report({
364
+ node: reportNode,
365
+ messageId: 'titleCase',
366
+ data: { text: checkable, suggestion },
367
+ suggest: [
368
+ {
369
+ messageId: 'titleCase',
370
+ data: { text: checkable, suggestion },
371
+ fix(fixer) {
372
+ if (reportNode.type === utils_1.AST_NODE_TYPES.Literal) {
373
+ const raw = context.getSourceCode().getText(reportNode);
374
+ const quote = raw[0];
375
+ return fixer.replaceText(reportNode, `${quote}${suggestion}${quote}`);
376
+ }
377
+ if (reportNode.type === utils_1.AST_NODE_TYPES.JSXText) {
378
+ const original = reportNode.value;
379
+ const replaced = original.replace(checkable, suggestion);
380
+ return fixer.replaceText(reportNode, replaced);
381
+ }
382
+ return null;
383
+ },
384
+ },
385
+ ],
386
+ });
387
+ }
388
+ }
389
+ return {
390
+ // Check inline JSX text like <Button>Back To App</Button>
391
+ JSXText(node) {
392
+ if (!checkJsxText)
393
+ return;
394
+ checkText(node.value, node);
395
+ },
396
+ // Check string literals in JSX attribute values
397
+ JSXAttribute(node) {
398
+ // Determine the attribute name (handles both plain and namespaced names)
399
+ let attrName;
400
+ if (node.name.type === utils_1.AST_NODE_TYPES.JSXNamespacedName) {
401
+ attrName = `${node.name.namespace.name}:${node.name.name.name}`;
402
+ }
403
+ else {
404
+ attrName = node.name.name;
405
+ }
406
+ if (!propsToCheckSet.has(attrName))
407
+ return;
408
+ const value = node.value;
409
+ if (!value)
410
+ return;
411
+ // <TextField label="Full Name" /> → value is a Literal
412
+ if (value.type === utils_1.AST_NODE_TYPES.Literal &&
413
+ typeof value.value === 'string') {
414
+ checkText(String(value.value), value);
415
+ return;
416
+ }
417
+ // <TextField label={"Full Name"} /> → value is JSXExpressionContainer
418
+ if (value.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
419
+ const expr = value.expression;
420
+ if (expr.type === utils_1.AST_NODE_TYPES.Literal &&
421
+ typeof expr.value === 'string') {
422
+ checkText(String(expr.value), expr);
423
+ }
424
+ // Template literals: skip — dynamic, cannot reliably check
425
+ }
426
+ },
427
+ };
428
+ },
429
+ });
430
+ //# sourceMappingURL=enforce-m3-sentence-case.js.map
@@ -0,0 +1,10 @@
1
+ type MessageIds = 'noFalsyCheck' | 'noRawTypeof';
2
+ type Options = [
3
+ {
4
+ snapshotHooks?: string[];
5
+ guardFunctions?: string[];
6
+ excludeFiles?: string[];
7
+ }?
8
+ ];
9
+ export declare const enforceSnapshotStateNarrowing: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
10
+ export {};