@pobammer-ts/eslint-cease-nonsense-rules 1.5.3 → 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.
Files changed (79) hide show
  1. package/README.md +594 -505
  2. package/dist/build-metadata.json +3 -3
  3. package/dist/index.d.ts +7 -1
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1374 -436
  6. package/dist/index.js.map +41 -35
  7. package/dist/oxfmt-sync.d.ts +4 -0
  8. package/dist/oxfmt-sync.d.ts.map +1 -0
  9. package/dist/oxfmt-worker.d.ts +12 -0
  10. package/dist/oxfmt-worker.d.ts.map +1 -0
  11. package/dist/oxfmt-worker.js +172786 -0
  12. package/dist/oxfmt-worker.js.map +27 -0
  13. package/dist/recognizers/camel-case-detector.d.ts +2 -2
  14. package/dist/recognizers/camel-case-detector.d.ts.map +1 -0
  15. package/dist/recognizers/code-recognizer.d.ts +6 -6
  16. package/dist/recognizers/code-recognizer.d.ts.map +1 -0
  17. package/dist/recognizers/contains-detector.d.ts +2 -2
  18. package/dist/recognizers/contains-detector.d.ts.map +1 -0
  19. package/dist/recognizers/detector.d.ts +3 -4
  20. package/dist/recognizers/detector.d.ts.map +1 -0
  21. package/dist/recognizers/end-with-detector.d.ts +2 -2
  22. package/dist/recognizers/end-with-detector.d.ts.map +1 -0
  23. package/dist/recognizers/javascript-footprint.d.ts +2 -2
  24. package/dist/recognizers/javascript-footprint.d.ts.map +1 -0
  25. package/dist/recognizers/keywords-detector.d.ts +2 -2
  26. package/dist/recognizers/keywords-detector.d.ts.map +1 -0
  27. package/dist/rules/ban-instances.d.ts +1 -0
  28. package/dist/rules/ban-instances.d.ts.map +1 -0
  29. package/dist/rules/ban-react-fc.d.ts +1 -0
  30. package/dist/rules/ban-react-fc.d.ts.map +1 -0
  31. package/dist/rules/enforce-ianitor-check-type.d.ts +2 -1
  32. package/dist/rules/enforce-ianitor-check-type.d.ts.map +1 -0
  33. package/dist/rules/fast-format.d.ts +36 -0
  34. package/dist/rules/fast-format.d.ts.map +1 -0
  35. package/dist/rules/no-async-constructor.d.ts +1 -0
  36. package/dist/rules/no-async-constructor.d.ts.map +1 -0
  37. package/dist/rules/no-color3-constructor.d.ts +1 -0
  38. package/dist/rules/no-color3-constructor.d.ts.map +1 -0
  39. package/dist/rules/no-commented-code.d.ts +1 -0
  40. package/dist/rules/no-commented-code.d.ts.map +1 -0
  41. package/dist/rules/no-god-components.d.ts +14 -0
  42. package/dist/rules/no-god-components.d.ts.map +1 -0
  43. package/dist/rules/no-identity-map.d.ts +9 -0
  44. package/dist/rules/no-identity-map.d.ts.map +1 -0
  45. package/dist/rules/no-instance-methods-without-this.d.ts +1 -0
  46. package/dist/rules/no-instance-methods-without-this.d.ts.map +1 -0
  47. package/dist/rules/no-print.d.ts +1 -0
  48. package/dist/rules/no-print.d.ts.map +1 -0
  49. package/dist/rules/no-shorthand-names.d.ts +1 -0
  50. package/dist/rules/no-shorthand-names.d.ts.map +1 -0
  51. package/dist/rules/no-useless-use-spring.d.ts +1 -0
  52. package/dist/rules/no-useless-use-spring.d.ts.map +1 -0
  53. package/dist/rules/no-warn.d.ts +1 -0
  54. package/dist/rules/no-warn.d.ts.map +1 -0
  55. package/dist/rules/prefer-sequence-overloads.d.ts +1 -0
  56. package/dist/rules/prefer-sequence-overloads.d.ts.map +1 -0
  57. package/dist/rules/prefer-udim2-shorthand.d.ts +1 -0
  58. package/dist/rules/prefer-udim2-shorthand.d.ts.map +1 -0
  59. package/dist/rules/require-named-effect-functions.d.ts +4 -3
  60. package/dist/rules/require-named-effect-functions.d.ts.map +1 -0
  61. package/dist/rules/require-paired-calls.d.ts +1 -0
  62. package/dist/rules/require-paired-calls.d.ts.map +1 -0
  63. package/dist/rules/require-react-component-keys.d.ts +1 -0
  64. package/dist/rules/require-react-component-keys.d.ts.map +1 -0
  65. package/dist/rules/use-exhaustive-dependencies.d.ts +1 -0
  66. package/dist/rules/use-exhaustive-dependencies.d.ts.map +1 -0
  67. package/dist/rules/use-hook-at-top-level.d.ts +1 -0
  68. package/dist/rules/use-hook-at-top-level.d.ts.map +1 -0
  69. package/dist/types/oxfmt.d.ts +88 -0
  70. package/dist/types/oxfmt.d.ts.map +1 -0
  71. package/dist/{configure-utilities.d.ts → utilities/configure-utilities.d.ts} +18 -10
  72. package/dist/utilities/configure-utilities.d.ts.map +1 -0
  73. package/dist/utilities/error-utilities.d.ts +25 -0
  74. package/dist/utilities/error-utilities.d.ts.map +1 -0
  75. package/dist/utilities/format-utilities.d.ts +20 -0
  76. package/dist/utilities/format-utilities.d.ts.map +1 -0
  77. package/dist/utilities/typebox-utilities.d.ts +4 -0
  78. package/dist/utilities/typebox-utilities.d.ts.map +1 -0
  79. package/package.json +32 -19
package/README.md CHANGED
@@ -1,8 +1,19 @@
1
1
  # eslint-cease-nonsense-rules
2
2
 
3
- A bunch of lints to prevent idiot mistakes I encounter with frequency.
3
+ An ESLint plugin that catches common mistakes before they reach production. This collection of rules helps prevent patterns that lead to bugs, performance issues, and maintainability problems.
4
4
 
5
- **NOTE** I did not write this junk, an LLM did because I do not care.
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Usage](#usage)
9
+ - [Rules](#rules)
10
+ - [Type Safety](#type-safety)
11
+ - [React](#react)
12
+ - [Logging](#logging)
13
+ - [Resource Management](#resource-management)
14
+ - [Code Quality](#code-quality)
15
+ - [Performance](#performance)
16
+ - [License](#license)
6
17
 
7
18
  ## Installation
8
19
 
@@ -12,41 +23,50 @@ bun add -D @pobammer-ts/eslint-cease-nonsense-rules
12
23
 
13
24
  ## Usage
14
25
 
15
- Add to your ESLint config:
26
+ ### Basic Setup
27
+
28
+ Add the plugin to your ESLint configuration:
16
29
 
17
30
  ```typescript
18
31
  import ceaseNonsense from "@pobammer-ts/eslint-cease-nonsense-rules";
19
32
 
20
33
  export default [
21
- {
22
- plugins: {
23
- "cease-nonsense": ceaseNonsense,
24
- },
25
- rules: {
26
- // Enable all rules (recommended)
27
- "cease-nonsense/ban-react-fc": "error",
28
- "cease-nonsense/enforce-ianitor-check-type": "error",
29
- "cease-nonsense/no-color3-constructor": "error",
30
- "cease-nonsense/no-instance-methods-without-this": "error",
31
- "cease-nonsense/no-print": "error",
32
- "cease-nonsense/no-shorthand-names": "error",
33
- "cease-nonsense/no-warn": "error",
34
- "cease-nonsense/prefer-sequence-overloads": "error",
35
- "cease-nonsense/prefer-udim2-shorthand": "error",
36
- "cease-nonsense/require-named-effect-functions": "error",
37
- "cease-nonsense/require-paired-calls": "error",
38
- "cease-nonsense/require-react-component-keys": "error",
39
- "cease-nonsense/no-useless-use-spring": "error",
40
- "cease-nonsense/use-exhaustive-dependencies": "error",
41
- "cease-nonsense/use-hook-at-top-level": "error",
42
- },
43
- },
34
+ {
35
+ plugins: {
36
+ "cease-nonsense": ceaseNonsense,
37
+ },
38
+ rules: {
39
+ // Enable all rules (recommended)
40
+ "cease-nonsense/ban-instances": "error",
41
+ "cease-nonsense/ban-react-fc": "error",
42
+ "cease-nonsense/enforce-ianitor-check-type": "error",
43
+ "cease-nonsense/fast-format": "error",
44
+ "cease-nonsense/no-async-constructor": "error",
45
+ "cease-nonsense/no-color3-constructor": "error",
46
+ "cease-nonsense/no-commented-code": "error",
47
+ "cease-nonsense/no-god-components": "error",
48
+ "cease-nonsense/no-identity-map": "error",
49
+ "cease-nonsense/no-instance-methods-without-this": "error",
50
+ "cease-nonsense/no-print": "error",
51
+ "cease-nonsense/no-shorthand-names": "error",
52
+ "cease-nonsense/no-useless-use-spring": "error",
53
+ "cease-nonsense/no-warn": "error",
54
+ "cease-nonsense/prefer-sequence-overloads": "error",
55
+ "cease-nonsense/prefer-udim2-shorthand": "error",
56
+ "cease-nonsense/require-named-effect-functions": "error",
57
+ "cease-nonsense/require-paired-calls": "error",
58
+ "cease-nonsense/require-react-component-keys": "error",
59
+ "cease-nonsense/use-exhaustive-dependencies": "error",
60
+ "cease-nonsense/use-hook-at-top-level": "error",
61
+ },
62
+ },
44
63
  ];
64
+ ```
45
65
 
46
- // Or just include the preset
47
- export default [
48
- ceaseNonsense.configs.recommended,
49
- ];
66
+ ### Using the Preset
67
+
68
+ ```typescript
69
+ export default [ceaseNonsense.configs.recommended];
50
70
  ```
51
71
 
52
72
  ## Rules
@@ -55,36 +75,50 @@ export default [
55
75
 
56
76
  #### `enforce-ianitor-check-type`
57
77
 
58
- Enforces `Ianitor.Check<T>` type annotations on complex TypeScript types to ensure runtime validation. You don't really need this.
78
+ Enforces `Ianitor.Check<T>` type annotations on complex TypeScript types to ensure runtime validation.
59
79
 
60
80
  Calculates structural complexity of types and requires Ianitor validators when complexity exceeds thresholds.
61
81
 
62
- **❌ Bad:**
82
+ **Configuration**
83
+
84
+ ```typescript
85
+ {
86
+ "cease-nonsense/enforce-ianitor-check-type": ["error", {
87
+ "baseThreshold": 10, // Minimum complexity to require validation
88
+ "warnThreshold": 15, // Warning threshold
89
+ "errorThreshold": 25, // Error threshold
90
+ "interfacePenalty": 20, // Complexity penalty for interfaces
91
+ "performanceMode": true // Enable performance optimizations
92
+ }]
93
+ }
94
+ ```
95
+
96
+ **❌ Bad**
63
97
 
64
98
  ```typescript
65
99
  // Complex type without runtime validation
66
100
  type UserConfig = {
67
- id: number;
68
- name: string;
69
- settings: {
70
- theme: string;
71
- notifications: boolean;
72
- };
101
+ id: number;
102
+ name: string;
103
+ settings: {
104
+ theme: string;
105
+ notifications: boolean;
106
+ };
73
107
  };
74
108
 
75
109
  const config = getUserConfig(); // No runtime check!
76
110
  ```
77
111
 
78
- **✅ Good:**
112
+ **✅ Good**
79
113
 
80
114
  ```typescript
81
115
  const userConfigValidator = Ianitor.interface({
82
- id: Ianitor.number(),
83
- name: Ianitor.string(),
84
- settings: Ianitor.interface({
85
- theme: Ianitor.string(),
86
- notifications: Ianitor.boolean(),
87
- }),
116
+ id: Ianitor.number(),
117
+ name: Ianitor.string(),
118
+ settings: Ianitor.interface({
119
+ theme: Ianitor.string(),
120
+ notifications: Ianitor.boolean(),
121
+ }),
88
122
  });
89
123
 
90
124
  type UserConfig = Ianitor.Static<typeof userConfigValidator>;
@@ -92,29 +126,15 @@ type UserConfig = Ianitor.Static<typeof userConfigValidator>;
92
126
  const config = userConfigValidator.check(getUserConfig());
93
127
  ```
94
128
 
95
- **Configuration:**
96
-
97
- ```typescript
98
- {
99
- "cease-nonsense/enforce-ianitor-check-type": ["error", {
100
- "baseThreshold": 10, // Minimum complexity to require validation
101
- "warnThreshold": 15, // Warning threshold
102
- "errorThreshold": 25, // Error threshold
103
- "interfacePenalty": 20, // Complexity penalty for interfaces
104
- "performanceMode": true // Enable performance optimizations
105
- }]
106
- }
107
- ```
108
-
109
129
  ### React
110
130
 
111
131
  #### `ban-react-fc`
112
132
 
113
133
  Bans React.FC and similar component type annotations. Use explicit function declarations instead.
114
134
 
115
- React.FC (Function Component) and related types break debug information in React DevTools, making profiling exponentially harder. They also encourage poor patterns and add unnecessary complexity.
135
+ React.FC types break debug information in React DevTools and encourage poor patterns.
116
136
 
117
- **❌ Bad:**
137
+ **❌ Bad**
118
138
 
119
139
  ```typescript
120
140
  export const MyComponent: React.FC<Props> = ({ children }) => {
@@ -128,7 +148,7 @@ const Modal: React.FunctionComponent = () => <div>Modal</div>;
128
148
  const Input: VFC = () => <input />;
129
149
  ```
130
150
 
131
- **✅ Good:**
151
+ **✅ Good**
132
152
 
133
153
  ```typescript
134
154
  export function MyComponent({ children }: Props) {
@@ -148,11 +168,51 @@ function Input() {
148
168
  }
149
169
  ```
150
170
 
171
+ #### `no-god-components`
172
+
173
+ Flags React components that are too large or doing too much, encouraging better separation of concerns.
174
+
175
+ **Default thresholds**
176
+
177
+ - Component body line count: target `120`, hard max `200`
178
+ - TSX nesting depth ≤ `3`
179
+ - Stateful hooks ≤ `5`
180
+ - Destructured props in parameters ≤ `5`
181
+ - Runtime `null` literals are always banned
182
+
183
+ **Configuration**
184
+
185
+ ```typescript
186
+ {
187
+ "cease-nonsense/no-god-components": ["error", {
188
+ targetLines: 120,
189
+ maxLines: 200,
190
+ maxTsxNesting: 3,
191
+ maxStateHooks: 5,
192
+ stateHooks: ["useState", "useReducer", "useBinding"],
193
+ maxDestructuredProps: 5,
194
+ enforceTargetLines: true,
195
+ ignoreComponents: ["LegacyComponent"]
196
+ }]
197
+ }
198
+ ```
199
+
151
200
  #### `require-react-component-keys`
152
201
 
153
202
  Enforces key props on all React elements except top-level returns from components.
154
203
 
155
- **❌ Bad:**
204
+ **Configuration**
205
+
206
+ ```typescript
207
+ {
208
+ "cease-nonsense/require-react-component-keys": ["error", {
209
+ "allowRootKeys": false, // Allow keys on root returns
210
+ "ignoreCallExpressions": ["ReactTree.mount"] // Functions to ignore
211
+ }]
212
+ }
213
+ ```
214
+
215
+ **❌ Bad**
156
216
 
157
217
  ```typescript
158
218
  function UserList({ users }) {
@@ -166,115 +226,127 @@ function UserList({ users }) {
166
226
  }
167
227
  ```
168
228
 
229
+ **✅ Good**
230
+
231
+ ```typescript
232
+ function UserList({ users }) {
233
+ return (
234
+ <div>
235
+ {users.map(user => (
236
+ <UserCard key={user.id} user={user} />
237
+ ))}
238
+ </div>
239
+ );
240
+ }
241
+ ```
242
+
169
243
  #### `require-named-effect-functions`
170
244
 
171
245
  Enforce named effect functions for better debuggability. Prevent inline arrow functions in `useEffect` and similar hooks.
172
246
 
173
- Behavior by environment (option `environment`):
247
+ **Behavior by environment**
174
248
 
175
- - `roblox-ts` (default): only identifiers allowed (e.g., `useEffect(onTick, [...])`). Inline function expressions or arrows are reported.
176
- - `standard`: identifiers and named function expressions are allowed; arrows are still reported.
249
+ - `roblox-ts` (default): Only identifiers allowed (e.g., `useEffect(onTick, [...])`)
250
+ - `standard`: Identifiers and named function expressions allowed
177
251
 
178
- Default hooks checked: `useEffect`, `useLayoutEffect`, `useInsertionEffect`.
252
+ **Configuration**
179
253
 
180
- **❌ Bad:**
254
+ ```typescript
255
+ {
256
+ "cease-nonsense/require-named-effect-functions": ["error", {
257
+ "environment": "roblox-ts", // or "standard"
258
+ "hooks": ["useEffect", "useLayoutEffect", "useInsertionEffect"]
259
+ }]
260
+ }
261
+ ```
262
+
263
+ **❌ Bad**
181
264
 
182
265
  ```typescript
183
266
  // Arrow function
184
267
  useEffect(() => {
185
- doThing();
268
+ doThing();
186
269
  }, [dep]);
187
270
 
188
271
  // Anonymous function expression
189
- useEffect(function () {
190
- doThing();
191
- }, [dep]);
272
+ useEffect(
273
+ function () {
274
+ doThing();
275
+ },
276
+ [dep],
277
+ );
192
278
  ```
193
279
 
194
- **✅ Good:**
280
+ **✅ Good**
195
281
 
196
282
  ```typescript
197
283
  // Preferred: reference a named function
198
284
  function onDepChange() {
199
- doThing();
285
+ doThing();
200
286
  }
201
287
  useEffect(onDepChange, [dep]);
202
288
 
203
289
  // Allowed in `standard` mode
204
- useEffect(function onDepChange() {
205
- doThing();
206
- }, [dep]);
290
+ useEffect(
291
+ function onDepChange() {
292
+ doThing();
293
+ },
294
+ [dep],
295
+ );
207
296
  ```
208
297
 
209
- **Configuration:**
298
+ #### `use-exhaustive-dependencies`
299
+
300
+ Enforces exhaustive and correct dependency specification in React hooks to prevent stale closures and unnecessary re-renders.
301
+
302
+ **Configuration**
210
303
 
211
304
  ```typescript
212
305
  {
213
- "cease-nonsense/require-named-effect-functions": ["error", {
214
- "environment": "roblox-ts", // or "standard"
215
- "hooks": ["useEffect", "useLayoutEffect", "useInsertionEffect"]
306
+ "cease-nonsense/use-exhaustive-dependencies": ["error", {
307
+ "reportMissingDependenciesArray": true,
308
+ "reportUnnecessaryDependencies": true,
309
+ "hooks": [
310
+ {
311
+ "name": "useCustomHook",
312
+ "closureIndex": 0,
313
+ "dependenciesIndex": 1,
314
+ "stableResult": true
315
+ }
316
+ ]
216
317
  }]
217
318
  }
218
319
  ```
219
320
 
220
- **✅ Good:**
321
+ **❌ Bad**
221
322
 
222
323
  ```typescript
223
- function UserList({ users }) {
224
- return (
225
- <div>
226
- {users.map(user => (
227
- <UserCard key={user.id} user={user} />
228
- ))}
229
- </div>
230
- );
231
- }
232
- ```
233
-
234
- **Configuration:**
324
+ function UserProfile({ userId }) {
325
+ const [user, setUser] = useState(null);
235
326
 
236
- ```typescript
237
- {
238
- "cease-nonsense/require-react-component-keys": ["error", {
239
- "allowRootKeys": false, // Allow keys on root returns
240
- "ignoreCallExpressions": ["ReactTree.mount"] // Functions to ignore
241
- }]
327
+ useEffect(() => {
328
+ fetchUser(userId).then(setUser);
329
+ }, []); // Missing userId dependency!
242
330
  }
243
331
  ```
244
332
 
245
- #### `use-exhaustive-dependencies`
246
-
247
- Enforces exhaustive and correct dependency specification in React hooks to prevent stale closures and unnecessary re-renders.
248
-
249
- **❌ Bad:**
333
+ **✅ Good**
250
334
 
251
335
  ```typescript
252
336
  function UserProfile({ userId }) {
253
- const [user, setUser] = useState(null);
337
+ const [user, setUser] = useState(null);
254
338
 
255
- useEffect(() => {
256
- fetchUser(userId).then(setUser);
257
- }, []); // Missing userId dependency!
339
+ useEffect(() => {
340
+ fetchUser(userId).then(setUser);
341
+ }, [userId]);
258
342
  }
259
343
  ```
260
344
 
261
345
  #### `no-useless-use-spring`
262
346
 
263
- Flags `useSpring`-style hooks that never change (static config plus non-updating deps). Defaults: `springHooks: ["useSpring"]`, `treatEmptyDepsAsViolation: true`.
347
+ Flags `useSpring`-style hooks that never change (static config plus non-updating deps).
264
348
 
265
- **❌ Bad:**
266
-
267
- ```typescript
268
- const spring = useSpring({ opacity: 1 }, []);
269
- ```
270
-
271
- **✅ Good:**
272
-
273
- ```typescript
274
- const spring = useSpring({ opacity: isOpen ? 1 : 0 }, [isOpen]);
275
- ```
276
-
277
- **Configuration:**
349
+ **Configuration**
278
350
 
279
351
  ```typescript
280
352
  {
@@ -285,112 +357,64 @@ const spring = useSpring({ opacity: isOpen ? 1 : 0 }, [isOpen]);
285
357
  }
286
358
  ```
287
359
 
288
- **✅ Good:**
360
+ **❌ Bad**
289
361
 
290
362
  ```typescript
291
- function UserProfile({ userId }) {
292
- const [user, setUser] = useState(null);
293
-
294
- useEffect(() => {
295
- fetchUser(userId).then(setUser);
296
- }, [userId]);
297
- }
363
+ const spring = useSpring({ opacity: 1 }, []);
298
364
  ```
299
365
 
300
- **Configuration:**
366
+ **✅ Good**
301
367
 
302
368
  ```typescript
303
- {
304
- "cease-nonsense/use-exhaustive-dependencies": ["error", {
305
- "reportMissingDependenciesArray": true,
306
- "reportUnnecessaryDependencies": true,
307
- "hooks": [
308
- {
309
- "name": "useCustomHook",
310
- "closureIndex": 0,
311
- "dependenciesIndex": 1,
312
- "stableResult": true
313
- }
314
- ]
315
- }]
316
- }
369
+ const spring = useSpring({ opacity: isOpen ? 1 : 0 }, [isOpen]);
317
370
  ```
318
371
 
319
372
  #### `use-hook-at-top-level`
320
373
 
321
374
  Enforces that React hooks are only called at the top level of components or custom hooks, never conditionally or in nested functions.
322
375
 
323
- **❌ Bad:**
324
-
325
- ```typescript
326
- function UserProfile({ userId }) {
327
- if (userId) {
328
- useEffect(() => { // Hook in conditional!
329
- fetchUser(userId);
330
- }, [userId]);
331
- }
332
- }
333
- ```
334
-
335
- **✅ Good:**
336
-
337
- ```typescript
338
- function UserProfile({ userId }) {
339
- useEffect(() => {
340
- if (userId) {
341
- fetchUser(userId);
342
- }
343
- }, [userId]);
344
- }
345
- ```
346
-
347
- **Configuration:**
376
+ **Configuration**
348
377
 
349
378
  ```typescript
350
379
  {
351
380
  "cease-nonsense/use-hook-at-top-level": ["error", {
352
381
  // Strategy 1: Ignore hooks by name
353
382
  "ignoreHooks": ["useEntity", "useComponent"],
354
-
355
- // Strategy 2: Control by import source (handles naming conflicts)
383
+
384
+ // Strategy 2: Control by import source
356
385
  "importSources": {
357
386
  "react": true, // Check hooks from React
358
387
  "my-ecs-library": false // Ignore ECS hooks
359
388
  },
360
-
361
- // Strategy 3: Whitelist mode (only check these hooks)
389
+
390
+ // Strategy 3: Whitelist mode
362
391
  "onlyHooks": ["useState", "useEffect", "useContext"]
363
392
  }]
364
393
  }
365
394
  ```
366
395
 
367
- **Examples:**
396
+ **❌ Bad**
368
397
 
369
398
  ```typescript
370
- // Ignore ECS hooks: { "ignoreHooks": ["useEntity"] }
371
- function Component() {
372
- if (condition) {
373
- useEntity(0); // Ignored
374
- }
375
- useState(0); // ❌ Error - still checked
399
+ function UserProfile({ userId }) {
400
+ if (userId) {
401
+ useEffect(() => {
402
+ // Hook in conditional!
403
+ fetchUser(userId);
404
+ }, [userId]);
405
+ }
376
406
  }
407
+ ```
377
408
 
378
- // Handle naming conflicts: { "importSources": { "react": true, "my-ecs": false } }
379
- import { useState } from 'react';
380
- import { useState as useEcsState } from 'my-ecs';
381
- function Component() {
382
- if (condition) {
383
- useState(0); // ❌ Error - React hook checked
384
- useEcsState(0); // ✅ Ignored - ECS hook ignored
385
- }
386
- }
409
+ **✅ Good**
387
410
 
388
- // Whitelist: { "onlyHooks": ["useState"] }
389
- function Component() {
390
- if (condition) {
391
- useState(0); // ❌ Error - in whitelist
392
- useEffect(f); // ✅ Ignored - not in whitelist
393
- }
411
+ ```typescript
412
+ function UserProfile({ userId }) {
413
+ useEffect(() => {
414
+ if (userId) {
415
+ fetchUser(userId);
416
+ }
417
+ }, [userId]);
394
418
  }
395
419
  ```
396
420
 
@@ -400,13 +424,13 @@ function Component() {
400
424
 
401
425
  Bans use of `print()` function calls. Use `Log` instead.
402
426
 
403
- **❌ Bad:**
427
+ **❌ Bad**
404
428
 
405
429
  ```typescript
406
430
  print("Debug message");
407
431
  ```
408
432
 
409
- **✅ Good:**
433
+ **✅ Good**
410
434
 
411
435
  ```typescript
412
436
  Log.info("Debug message");
@@ -416,13 +440,13 @@ Log.info("Debug message");
416
440
 
417
441
  Bans use of `warn()` function calls. Use `Log` instead.
418
442
 
419
- **❌ Bad:**
443
+ **❌ Bad**
420
444
 
421
445
  ```typescript
422
446
  warn("Warning message");
423
447
  ```
424
448
 
425
- **✅ Good:**
449
+ **✅ Good**
426
450
 
427
451
  ```typescript
428
452
  Log.warn("Warning message");
@@ -432,311 +456,411 @@ Log.warn("Warning message");
432
456
 
433
457
  #### `require-paired-calls`
434
458
 
435
- Enforces that paired function calls (opener/closer) are properly balanced across all execution paths with LIFO ordering. Prevents resource leaks, unbalanced operations, and control flow errors.
436
-
437
- **Key features:**
459
+ Enforces that paired function calls (opener/closer) are properly balanced across all execution paths with LIFO ordering.
438
460
 
439
- - Control flow analysis across all paths (if/else, switch, try/catch, loops)
440
- - Early exit detection (return, throw, break, continue)
441
- - LIFO validation for nested pairs
442
- - Async operation detection (await/yield) with `requireSync`
443
- - Roblox-specific auto-close detection for yielding functions
444
-
445
- **❌ Bad:**
461
+ **Configuration**
446
462
 
447
463
  ```typescript
448
- // Missing closer on early return
449
- function test() {
450
- debug.profilebegin("task");
451
- if (error) return; // profilebegin never closed on this path
452
- debug.profileend();
464
+ {
465
+ "cease-nonsense/require-paired-calls": ["error", {
466
+ "pairs": [{
467
+ "opener": "debug.profilebegin",
468
+ "closer": "debug.profileend",
469
+ "alternatives": ["db.rollback"],
470
+ "requireSync": true,
471
+ "platform": "roblox",
472
+ "yieldingFunctions": [
473
+ "task.wait",
474
+ "*.WaitForChild"
475
+ ]
476
+ }],
477
+ "allowConditionalClosers": false,
478
+ "allowMultipleOpeners": true,
479
+ "maxNestingDepth": 0
480
+ }]
453
481
  }
482
+ ```
454
483
 
455
- // Unpaired closer (no matching opener)
456
- function test() {
457
- doWork();
458
- debug.profileend(); // No matching profilebegin
459
- }
484
+ **Pair configuration options**
460
485
 
461
- // Wrong LIFO order
462
- function test() {
463
- debug.profilebegin("outer");
464
- debug.profilebegin("inner");
465
- debug.profileend(); // closes inner
466
- // outer is never closed
467
- }
486
+ - `opener` (required): Function name that starts the paired operation
487
+ - `openerAlternatives` (optional): Additional opener names sharing the same closer
488
+ - `closer` (required): Function name(s) that close the operation
489
+ - `alternatives` (optional): Alternative closers for error paths
490
+ - `requireSync` (optional): Disallow `await`/`yield` between opener and closer
491
+ - `platform` (optional): Enables `"roblox"`-specific behavior
492
+ - `yieldingFunctions` (optional): Custom patterns for Roblox yielding functions (supports wildcards)
468
493
 
469
- // Async operation with requireSync: true
470
- async function test() {
471
- debug.profilebegin("task");
472
- await fetch("/api"); // Cannot await between profilebegin/end
473
- debug.profileend();
474
- }
494
+ **Top-level options**
475
495
 
476
- // Roblox yielding function auto-closes
477
- function test() {
478
- debug.profilebegin("task");
479
- task.wait(1); // Auto-closes all profiles
480
- debug.profileend(); // This will error - already closed
481
- }
496
+ - `pairs` (required): Array of pair configurations
497
+ - `allowConditionalClosers` (optional): Allow closers in some but not all branches
498
+ - `allowMultipleOpeners` (optional): Allow consecutive opener calls
499
+ - `maxNestingDepth` (optional): Maximum nesting depth (0 = unlimited)
482
500
 
483
- // Conditional branch missing closer
484
- function test() {
485
- debug.profilebegin("task");
486
- if (condition) {
487
- debug.profileend();
488
- } else {
489
- return; // Missing closer on this path
490
- }
501
+ **Default configuration (Roblox profiling)**
502
+
503
+ ```typescript
504
+ {
505
+ "cease-nonsense/require-paired-calls": ["error", {
506
+ "pairs": [{
507
+ "opener": "debug.profilebegin",
508
+ "closer": "debug.profileend",
509
+ "platform": "roblox",
510
+ "requireSync": true,
511
+ "yieldingFunctions": ["task.wait", "wait", "*.WaitForChild"]
512
+ }]
513
+ }]
491
514
  }
515
+ ```
516
+
517
+ **❌ Bad**
492
518
 
493
- // Break/continue skip closer
519
+ ```typescript
520
+ // Missing closer on early return
494
521
  function test() {
495
- debug.profilebegin("loop");
496
- for (const item of items) {
497
- if (item.stop) break; // Skips closer
498
- }
499
- debug.profileend();
522
+ debug.profilebegin("task");
523
+ if (error) return; // Never closed on this path
524
+ debug.profileend();
500
525
  }
501
526
 
502
- // Contextual error: empty stack
527
+ // Wrong LIFO order
503
528
  function test() {
504
- Iris.End(); // Error: Unexpected call to 'Iris.End' - no matching opener on stack
529
+ debug.profilebegin("outer");
530
+ debug.profilebegin("inner");
531
+ debug.profileend(); // closes inner
532
+ // outer is never closed
505
533
  }
506
534
 
507
- // Contextual error: wrong closer
508
- function test() {
509
- Iris.CollapsingHeader(["Units"]);
510
- debug.profileend(); // Error: Unexpected call to 'debug.profileend' - expected one of: Iris.End
535
+ // Async operation with requireSync
536
+ async function test() {
537
+ debug.profilebegin("task");
538
+ await fetch("/api");
539
+ debug.profileend();
511
540
  }
512
541
  ```
513
542
 
514
- **✅ Good:**
543
+ **✅ Good**
515
544
 
516
545
  ```typescript
517
546
  // Simple pairing
518
547
  function test() {
519
- debug.profilebegin("task");
520
- doWork();
521
- debug.profileend();
548
+ debug.profilebegin("task");
549
+ doWork();
550
+ debug.profileend();
522
551
  }
523
552
 
524
553
  // Proper LIFO nesting
525
554
  function test() {
526
- debug.profilebegin("outer");
527
- debug.profilebegin("inner");
528
- debug.profileend(); // closes inner
529
- debug.profileend(); // closes outer
555
+ debug.profilebegin("outer");
556
+ debug.profilebegin("inner");
557
+ debug.profileend();
558
+ debug.profileend();
530
559
  }
531
560
 
532
561
  // Try-finally ensures closer on all paths
533
562
  function test() {
534
- debug.profilebegin("task");
535
- try {
536
- riskyOperation();
537
- } finally {
538
- debug.profileend();
539
- }
563
+ debug.profilebegin("task");
564
+ try {
565
+ riskyOperation();
566
+ } finally {
567
+ debug.profileend();
568
+ }
540
569
  }
570
+ ```
541
571
 
542
- // Try-catch with alternative closers
543
- function test() {
544
- db.transaction();
545
- try {
546
- db.users.insert({ name: "test" });
547
- db.commit(); // Normal closer
548
- } catch (err) {
549
- db.rollback(); // Alternative closer
550
- throw err;
551
- }
552
- }
572
+ **Real-world examples**
553
573
 
554
- // Closer in all branches
555
- function test() {
556
- debug.profilebegin("task");
557
- if (condition) {
558
- debug.profileend();
559
- } else {
560
- debug.profileend();
561
- }
574
+ ```typescript
575
+ // Database transactions
576
+ {
577
+ "pairs": [{
578
+ "opener": "db.transaction",
579
+ "closer": "db.commit",
580
+ "alternatives": ["db.rollback"]
581
+ }]
562
582
  }
563
583
 
564
- // Pairs inside loop iterations (not across)
565
- function test() {
566
- for (const item of items) {
567
- debug.profilebegin("item");
568
- process(item);
569
- debug.profileend();
570
- }
584
+ // Lock acquire/release
585
+ {
586
+ "pairs": [{
587
+ "opener": "lock.acquire",
588
+ "closer": ["lock.release", "lock.free"]
589
+ }]
571
590
  }
572
591
 
573
- // Multiple closers with requireAll
574
- function test() {
575
- resource.init();
576
- resource.setup();
577
- // Both cleanup1 and cleanup2 must be called
578
- resource.cleanup1();
579
- resource.cleanup2();
592
+ // Roblox Iris widgets
593
+ {
594
+ "pairs": [{
595
+ "opener": "Iris.CollapsingHeader",
596
+ "openerAlternatives": ["Iris.Window", "Iris.TreeNode", "Iris.Table"],
597
+ "closer": "Iris.End",
598
+ "platform": "roblox",
599
+ "requireSync": true
600
+ }]
580
601
  }
581
602
  ```
582
603
 
583
- **Configuration:**
604
+ ### Code Quality
605
+
606
+ #### `ban-instances`
607
+
608
+ Bans specified Roblox Instance classes in `new Instance()` calls and JSX elements.
609
+
610
+ **Configuration**
584
611
 
585
612
  ```typescript
613
+ // Array format (default message)
586
614
  {
587
- "cease-nonsense/require-paired-calls": ["error", {
588
- "pairs": [{
589
- "opener": "debug.profilebegin", // Opener function name
590
- "closer": "debug.profileend", // Closer function name(s)
591
- "alternatives": ["db.rollback"], // Alternative closers (any one)
592
- "requireSync": true, // Disallow await/yield
593
- "platform": "roblox", // Platform-specific behavior
594
- "yieldingFunctions": [ // Custom yielding patterns
595
- "task.wait",
596
- "*.WaitForChild" // Supports wildcards
597
- ]
598
- }],
599
- "allowConditionalClosers": false, // Allow closers in some branches
600
- "allowMultipleOpeners": true, // Allow consecutive openers
601
- "maxNestingDepth": 0 // Nesting limit (0 = unlimited)
615
+ "cease-nonsense/ban-instances": ["error", {
616
+ "bannedInstances": ["Part", "Script", "LocalScript"]
617
+ }]
618
+ }
619
+
620
+ // Object format (custom messages)
621
+ {
622
+ "cease-nonsense/ban-instances": ["error", {
623
+ "bannedInstances": {
624
+ "Part": "Use MeshPart instead for better performance",
625
+ "Script": "Scripts should not be created at runtime"
626
+ }
602
627
  }]
603
628
  }
604
629
  ```
605
630
 
606
- **Configuration Options:**
631
+ **❌ Bad**
607
632
 
608
- **Pair Configuration** (per-pair settings in the `pairs` array):
633
+ ```typescript
634
+ // With config: { bannedInstances: ["Part", "Script"] }
635
+ const part = new Instance("Part");
636
+ const script = new Instance("Script");
609
637
 
610
- - `opener` (required, string) - Function name that starts the paired operation (e.g., `"debug.profilebegin"`). Must be an exact function name including member access (e.g., `"obj.method"`).
638
+ // JSX (lowercase = Roblox Instance)
639
+ <part Size={new Vector3(1, 1, 1)} />
640
+ ```
611
641
 
612
- - `openerAlternatives` (optional, string[]) - Additional opener names that share the same closer. Every name in this array is treated exactly like `opener` (same stack entry, same diagnostics). Handy for APIs like Iris where `Iris.End` closes windows, collapsing headers, etc.
642
+ **✅ Good**
613
643
 
614
- - `closer` (required, string | string[]) - Function name(s) that close the paired operation. Can be:
615
- - Single string: `"debug.profileend"` - only this function can close
616
- - Array of strings: `["lock.release", "lock.free"]` - ANY of these functions can close (alternatives within closer)
644
+ ```typescript
645
+ const meshPart = new Instance("MeshPart");
646
+ <meshpart Size={new Vector3(1, 1, 1)} />
647
+ ```
617
648
 
618
- - Multiple pair configs can share the same closer. The rule will always pop the most recent opener whose configuration allows that closer, so you can keep separate telemetry per widget/button/etc. while still reusing a single `End()` call.
649
+ #### `fast-format`
619
650
 
620
- - `alternatives` (optional, string[]) - Alternative closer function names used for error paths. When present, ANY ONE of the `closer` or `alternatives` satisfies the requirement. Example: `"closer": "db.commit", "alternatives": ["db.rollback"]` means either commit OR rollback closes the transaction.
651
+ Enforces oxfmt code formatting. Reports INSERT, DELETE, and REPLACE operations for formatting differences.
621
652
 
622
- - `requireSync` (optional, boolean, default: `false`) - When `true`, disallows `await` or `yield` expressions between opener and closer. Reports error if async operations occur within the paired scope.
653
+ **Features**
623
654
 
624
- - `platform` (optional, `"roblox"`) - Enables Roblox-specific behavior:
625
- - Auto-detects yielding function calls (configured via `yieldingFunctions`)
626
- - When a yielding function is called, ALL open profiles are automatically closed
627
- - Subsequent closer calls after yielding will report errors (already closed)
655
+ - Has auto-fix
656
+ - Uses an LRU cache to avoid re-formatting unchanged files
628
657
 
629
- - `yieldingFunctions` (optional, string[], only with `platform: "roblox"`) - Custom patterns for Roblox yielding functions. Supports wildcards:
630
- - Exact match: `"task.wait"` matches only `task.wait()`
631
- - Wildcard method: `"*.WaitForChild"` matches `instance.WaitForChild()`, `player.WaitForChild()`, etc.
632
- - Default: `["task.wait", "wait", "*.WaitForChild"]`
658
+ #### `no-async-constructor`
633
659
 
634
- **Top-Level Options:**
660
+ Disallows asynchronous operations inside class constructors.
635
661
 
636
- - `pairs` (required, array) - Array of pair configurations to enforce. Rule checks all configured pairs simultaneously.
662
+ **Why**
637
663
 
638
- - `allowConditionalClosers` (optional, boolean, default: `false`) - Controls whether closers must be called on ALL execution paths:
639
- - `false` (strict): Requires closer on every path (if/else both branches, all switch cases, etc.)
640
- - `true` (permissive): Allows closers in some but not all branches
664
+ Constructors return immediately, so async work causes race conditions, unhandled rejections, and incomplete object states.
641
665
 
642
- - `allowMultipleOpeners` (optional, boolean, default: `true`) - Controls consecutive opener calls:
643
- - `true`: Allows multiple opener calls before closers (nesting)
644
- - `false`: Reports error if opener is called again before closer
666
+ **Detected violations**
645
667
 
646
- - `maxNestingDepth` (optional, number, default: `0`) - Maximum nesting depth for paired calls:
647
- - `0`: Unlimited nesting
648
- - `> 0`: Reports error if nesting exceeds this depth
668
+ - `await` expressions
669
+ - Promise chains (`.then()`, `.catch()`, `.finally()`)
670
+ - Async IIFEs (`(async () => {})()`)
671
+ - Unhandled async method calls (`this.asyncMethod()`)
672
+ - Orphaned promises (`const p = this.asyncMethod()`)
649
673
 
650
- **Default configuration (Roblox profiling):**
674
+ **❌ Bad**
651
675
 
652
676
  ```typescript
653
- {
654
- "cease-nonsense/require-paired-calls": ["error", {
655
- "pairs": [{
656
- "opener": "debug.profilebegin",
657
- "closer": "debug.profileend",
658
- "platform": "roblox",
659
- "requireSync": true,
660
- "yieldingFunctions": ["task.wait", "wait", "*.WaitForChild"]
661
- }]
662
- }]
677
+ class UserService {
678
+ constructor() {
679
+ await this.initialize(); // Direct await
680
+ this.loadData().then((data) => (this.data = data)); // Promise chain
681
+ (async () => {
682
+ await this.setup();
683
+ })(); // Async IIFE
684
+ }
685
+
686
+ async initialize() {
687
+ /* ... */
688
+ }
689
+ async loadData() {
690
+ /* ... */
691
+ }
692
+ async setup() {
693
+ /* ... */
694
+ }
663
695
  }
664
696
  ```
665
697
 
666
- **Real-world examples:**
698
+ **✅ Good**
667
699
 
668
700
  ```typescript
669
- // Database transactions
670
- {
671
- "pairs": [{
672
- "opener": "db.transaction",
673
- "closer": "db.commit",
674
- "alternatives": ["db.rollback"]
675
- }]
701
+ class UserService {
702
+ private initPromise: Promise<void>;
703
+
704
+ constructor() {
705
+ this.initPromise = this.initialize();
706
+ }
707
+
708
+ async initialize() {
709
+ /* ... */
710
+ }
711
+
712
+ // Factory pattern
713
+ static async create(): Promise<UserService> {
714
+ const service = new UserService();
715
+ await service.initPromise;
716
+ return service;
717
+ }
676
718
  }
719
+ ```
677
720
 
678
- // Lock acquire/release
679
- {
680
- "pairs": [{
681
- "opener": "lock.acquire",
682
- "closer": ["lock.release", "lock.free"]
683
- }]
721
+ #### `no-commented-code`
722
+
723
+ Detects and reports commented-out code.
724
+
725
+ **Features**
726
+
727
+ - 💡 Has suggestions
728
+ - Groups adjacent line comments and block comments
729
+ - Uses heuristic detection combined with parsing to minimize false positives
730
+
731
+ **❌ Bad**
732
+
733
+ ```typescript
734
+ function calculate(x: number) {
735
+ // const result = x * 2;
736
+ // return result;
737
+
738
+ /* if (x > 10) {
739
+ return x;
740
+ } */
741
+
742
+ return x + 1;
684
743
  }
744
+ ```
685
745
 
686
- // Roblox Iris widgets all using Iris.End
687
- {
688
- "pairs": [{
689
- "opener": "Iris.CollapsingHeader",
690
- "openerAlternatives": ["Iris.Window", "Iris.TreeNode", "Iris.Table"],
691
- "closer": "Iris.End",
692
- "platform": "roblox",
693
- "requireSync": true,
694
- "yieldingFunctions": ["task.wait", "wait", "*.WaitForChild", "*.*Async"]
695
- }]
746
+ **✅ Good**
747
+
748
+ ```typescript
749
+ function calculate(x: number) {
750
+ // TODO: Consider multiplying by 2 instead
751
+ // Note: This is a simplified version
752
+ return x + 1;
696
753
  }
754
+ ```
755
+
756
+ #### `no-identity-map`
757
+
758
+ Bans pointless identity `.map()` calls that return the parameter unchanged.
759
+
760
+ **Features**
761
+
762
+ - ✨ Has auto-fix
763
+ - Context-aware messages for Bindings vs Arrays
764
+
765
+ **❌ Bad**
766
+
767
+ ```typescript
768
+ // Bindings
769
+ const result = scaleBinding.map((value) => value);
770
+
771
+ // Arrays - pointless shallow copy
772
+ const copied = items.map((item) => item);
773
+ ```
774
+
775
+ **✅ Good**
776
+
777
+ ```typescript
778
+ // Bindings - use directly
779
+ const result = scaleBinding;
697
780
 
698
- // Multiple pairs
781
+ // Arrays - use table.clone or spread
782
+ const copied = table.clone(items);
783
+ const copied2 = [...items];
784
+
785
+ // Actual transformations are fine
786
+ const doubled = items.map((x) => x * 2);
787
+ ```
788
+
789
+ **Configuration**
790
+
791
+ ```typescript
699
792
  {
700
- "pairs": [
701
- { "opener": "debug.profilebegin", "closer": "debug.profileend" },
702
- { "opener": "db.transaction", "closer": "db.commit", "alternatives": ["db.rollback"] },
703
- { "opener": "file.open", "closer": "file.close" }
704
- ]
793
+ "cease-nonsense/no-identity-map": ["error", {
794
+ "bindingPatterns": ["binding"] // Case-insensitive patterns
795
+ }]
705
796
  }
797
+ ```
706
798
 
707
- // Strict nesting
799
+ #### `no-shorthand-names`
800
+
801
+ Bans shorthand variable names in favor of descriptive full names.
802
+
803
+ **Default mappings**
804
+
805
+ - `plr` → `player` (or `localPlayer` for `Players.LocalPlayer`)
806
+ - `args` → `parameters`
807
+ - `dt` → `deltaTime`
808
+ - `char` → `character`
809
+ - `btn` → `button`
810
+
811
+ **Configuration**
812
+
813
+ ```typescript
708
814
  {
709
- "pairs": [{ "opener": "begin", "closer": "end" }],
710
- "maxNestingDepth": 2,
711
- "allowMultipleOpeners": false
815
+ "cease-nonsense/no-shorthand-names": ["error", {
816
+ "shorthands": {
817
+ "plr": "player",
818
+ "args": "parameters",
819
+ "dt": "deltaTime",
820
+ "char": "character",
821
+ "btn": "button"
822
+ },
823
+ "allowPropertyAccess": ["char"] // Allow as property
824
+ }]
712
825
  }
713
826
  ```
714
827
 
715
- **Use cases:**
828
+ **❌ Bad**
716
829
 
717
- - Roblox/Luau profiling (debug.profilebegin/end)
718
- - Database transactions (transaction/commit/rollback)
719
- - Resource locks (acquire/release)
720
- - File handles (open/close)
721
- - Network connections (connect/disconnect)
722
- - Any begin/end API pattern
830
+ ```typescript
831
+ const plr = getPlayer();
832
+ const args = [1, 2, 3];
833
+ const dt = 0.016;
834
+ ```
723
835
 
724
- ### Code Quality
836
+ **✅ Good**
837
+
838
+ ```typescript
839
+ const player = getPlayer();
840
+ const localPlayer = Players.LocalPlayer;
841
+ const parameters = [1, 2, 3];
842
+ const deltaTime = 0.016;
843
+ const model = entity.char; // Property access is allowed
844
+ ```
845
+
846
+ ### Performance
725
847
 
726
848
  #### `no-color3-constructor`
727
849
 
728
- Bans `new Color3(...)` except for `new Color3()` or `new Color3(0, 0, 0)`. Use `Color3.fromRGB()` instead for better performance.
850
+ Bans `new Color3(...)` except for `new Color3()` or `new Color3(0, 0, 0)`. Use `Color3.fromRGB()` instead.
851
+
852
+ **Features**
729
853
 
730
- ##### ✨ Has auto-fix
854
+ - ✨ Has auto-fix
731
855
 
732
- **❌ Bad:**
856
+ **❌ Bad**
733
857
 
734
858
  ```typescript
735
859
  const red = new Color3(255, 0, 0);
736
860
  const blue = new Color3(0.5, 0.5, 1);
737
861
  ```
738
862
 
739
- **✅ Good:**
863
+ **✅ Good**
740
864
 
741
865
  ```typescript
742
866
  const red = Color3.fromRGB(255, 0, 0);
@@ -746,16 +870,21 @@ const black = new Color3(0, 0, 0); // Allowed
746
870
 
747
871
  #### `prefer-udim2-shorthand`
748
872
 
749
- Prefer `UDim2.fromScale()` or `UDim2.fromOffset()` when all offsets or all scales are zero. Leaves `new UDim2(0, 0, 0, 0)` alone. Includes auto-fix.
873
+ Prefer `UDim2.fromScale()` or `UDim2.fromOffset()` when all offsets or all scales are zero.
750
874
 
751
- **❌ Bad:**
875
+ **Features**
876
+
877
+ - ✨ Has auto-fix
878
+ - Leaves `new UDim2(0, 0, 0, 0)` alone
879
+
880
+ **❌ Bad**
752
881
 
753
882
  ```typescript
754
883
  new UDim2(1, 0, 1, 0);
755
884
  new UDim2(0, 100, 0, 50);
756
885
  ```
757
886
 
758
- **✅ Good:**
887
+ **✅ Good**
759
888
 
760
889
  ```typescript
761
890
  UDim2.fromScale(1, 1);
@@ -765,23 +894,25 @@ new UDim2(0, 0, 0, 0); // Allowed
765
894
 
766
895
  #### `prefer-sequence-overloads`
767
896
 
768
- Prefer the optimized `ColorSequence` and `NumberSequence` constructor overloads instead of building an array of `*SequenceKeypoint`s for the 0/1 endpoints. Includes auto-fix.
897
+ Prefer the optimized `ColorSequence` and `NumberSequence` constructor overloads instead of building an array of `*SequenceKeypoint`s.
898
+
899
+ **Features**
769
900
 
770
- **❌ Bad:**
901
+ - ✨ Has auto-fix
902
+ - Automatically collapses identical 0/1 endpoints
903
+
904
+ **❌ Bad**
771
905
 
772
906
  ```typescript
773
907
  new ColorSequence([
774
- new ColorSequenceKeypoint(0, Color3.fromRGB(100, 200, 255)),
775
- new ColorSequenceKeypoint(1, Color3.fromRGB(255, 100, 200)),
908
+ new ColorSequenceKeypoint(0, Color3.fromRGB(100, 200, 255)),
909
+ new ColorSequenceKeypoint(1, Color3.fromRGB(255, 100, 200)),
776
910
  ]);
777
911
 
778
- new NumberSequence([
779
- new NumberSequenceKeypoint(0, 0),
780
- new NumberSequenceKeypoint(1, 100),
781
- ]);
912
+ new NumberSequence([new NumberSequenceKeypoint(0, 0), new NumberSequenceKeypoint(1, 100)]);
782
913
  ```
783
914
 
784
- **✅ Good:**
915
+ **✅ Good**
785
916
 
786
917
  ```typescript
787
918
  new ColorSequence(Color3.fromRGB(100, 200, 255), Color3.fromRGB(255, 100, 200));
@@ -793,116 +924,74 @@ new NumberSequence(0, 100);
793
924
  new NumberSequence(42);
794
925
  ```
795
926
 
796
- Automatically collapses identical 0/1 endpoints to the single-argument overload.
797
-
798
927
  #### `no-instance-methods-without-this`
799
928
 
800
- Detects instance methods that don't use `this` and should be converted to standalone functions for better performance in roblox-ts.
929
+ Detects instance methods that don't use `this` and should be converted to standalone functions.
801
930
 
802
- In roblox-ts, instance methods create metatable objects with significant performance overhead. Methods that don't use `this` can be moved outside the class and called as standalone functions, eliminating this overhead entirely.
803
-
804
- **❌ Bad:**
805
-
806
- ```typescript
807
- type OnChange = (currentValue: number, previousValue: number) => void;
931
+ **Why**
808
932
 
809
- class MyClass {
810
- private readonly onChanges = new Array<OnChange>();
811
- private value = 0;
933
+ In roblox-ts, instance methods create metatable objects with significant performance overhead. Methods that don't use `this` can be moved outside the class.
812
934
 
813
- public increment(): void {
814
- const previousValue = this.value;
815
- const value = previousValue + 1;
816
- this.value = value;
817
- this.notifyChanges(value, previousValue); // ← Bad: method doesn't use this
818
- }
935
+ **Configuration**
819
936
 
820
- private notifyChanges(value: number, previousValue: number): void {
821
- for (const onChange of this.onChanges) onChange(value, previousValue);
822
- }
937
+ ```typescript
938
+ {
939
+ "cease-nonsense/no-instance-methods-without-this": ["error", {
940
+ "checkPrivate": true, // Default: true
941
+ "checkProtected": true, // Default: true
942
+ "checkPublic": true // Default: true
943
+ }]
823
944
  }
824
945
  ```
825
946
 
826
- **✅ Good:**
947
+ **❌ Bad**
827
948
 
828
949
  ```typescript
829
950
  type OnChange = (currentValue: number, previousValue: number) => void;
830
951
 
831
- function notifyChanges(value: number, previousValue: number, onChanges: ReadonlyArray<OnChange>): void {
832
- for (const onChange of onChanges) onChange(value, previousValue);
833
- }
834
-
835
952
  class MyClass {
836
- private readonly onChanges = new Array<OnChange>();
837
- private value = 0;
953
+ private readonly onChanges = new Array<OnChange>();
954
+ private value = 0;
838
955
 
839
- public increment(): void {
840
- const previousValue = this.value;
841
- const value = previousValue + 1;
842
- this.value = value;
843
- notifyChanges(value, previousValue, this.onChanges); // Standalone function call
844
- }
845
- }
846
- ```
956
+ public increment(): void {
957
+ const previousValue = this.value;
958
+ const value = previousValue + 1;
959
+ this.value = value;
960
+ this.notifyChanges(value, previousValue); // Doesn't use this
961
+ }
847
962
 
848
- **Configuration:**
849
-
850
- ```typescript
851
- {
852
- "cease-nonsense/no-instance-methods-without-this": ["error", {
853
- "checkPrivate": true, // Check private methods (default: true)
854
- "checkProtected": true, // Check protected methods (default: true)
855
- "checkPublic": true // Check public methods (default: true)
856
- }]
963
+ private notifyChanges(value: number, previousValue: number): void {
964
+ for (const onChange of this.onChanges) {
965
+ onChange(value, previousValue);
966
+ }
967
+ }
857
968
  }
858
969
  ```
859
970
 
860
- #### `no-shorthand-names`
861
-
862
- Bans shorthand variable names in favor of descriptive full names.
863
-
864
- **Default mappings:**
865
-
866
- - `plr` → `player` (or `localPlayer` for `Players.LocalPlayer`)
867
- - `args` → `parameters`
868
- - `dt` → `deltaTime`
869
- - `char` → `character`
870
-
871
- **❌ Bad:**
971
+ **✅ Good**
872
972
 
873
973
  ```typescript
874
- const plr = getPlayer();
875
- const args = [1, 2, 3];
876
- const dt = 0.016;
877
- ```
878
-
879
- **✅ Good:**
974
+ type OnChange = (currentValue: number, previousValue: number) => void;
880
975
 
881
- ```typescript
882
- const player = getPlayer();
883
- const localPlayer = Players.LocalPlayer;
884
- const parameters = [1, 2, 3];
885
- const deltaTime = 0.016;
886
- const model = entity.char; // Property access is allowed
887
- ```
976
+ function notifyChanges(value: number, previousValue: number, onChanges: ReadonlyArray<OnChange>): void {
977
+ for (const onChange of onChanges) {
978
+ onChange(value, previousValue);
979
+ }
980
+ }
888
981
 
889
- **Configuration:**
982
+ class MyClass {
983
+ private readonly onChanges = new Array<OnChange>();
984
+ private value = 0;
890
985
 
891
- ```typescript
892
- {
893
- "cease-nonsense/no-shorthand-names": ["error", {
894
- "shorthands": {
895
- "plr": "player",
896
- "args": "parameters",
897
- "dt": "deltaTime",
898
- "char": "character",
899
- "btn": "button" // Add custom mappings
900
- },
901
- "allowPropertyAccess": ["char"] // Allow as property
902
- }]
986
+ public increment(): void {
987
+ const previousValue = this.value;
988
+ const value = previousValue + 1;
989
+ this.value = value;
990
+ notifyChanges(value, previousValue, this.onChanges);
991
+ }
903
992
  }
904
993
  ```
905
994
 
906
995
  ## License
907
996
 
908
- Do whatever you want with this code. I don't care. I know it says MIT but that is genuinely just a formality.
997
+ MIT License - feel free to use this code however you want.