@deslop/coding-standards 0.1.1077 → 0.1.1079

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deslop/coding-standards",
3
- "version": "0.1.1077",
3
+ "version": "0.1.1079",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/MP281X/deslop",
@@ -136,13 +136,13 @@ const root = options?.root !== undefined ? options.root : '.'
136
136
  ```tsx
137
137
  // good
138
138
  if (enabled) return
139
- aria-current={props.selected === true ? 'page' : undefined}
139
+ aria-current={Boolean.match(props.selected === true, {onFalse: () => undefined, onTrue: () => 'page'})}
140
140
  // bad
141
141
  if (enabled === true) return // enabled is a boolean
142
142
  if (props.selected) return // props.selected can be undefined
143
143
  ```
144
144
 
145
- **Flat.** Use early-return statements in Effect.fn. No nested ternaries or combinators deeper than equivalent statements. Match and combinators serve single expressions; Boolean.match selects a value, except one-line JSX-attribute ternaries.
145
+ **Flat.** Use early-return statements in Effect.fn. No ternaries, in JSX too, and no combinators deeper than equivalent statements. An early return, `Boolean.match`, `Option.match`, `Array.match`, `Match` or a lookup record selects a value.
146
146
 
147
147
  ```ts
148
148
  // good
@@ -155,6 +155,7 @@ Boolean.match(ignoreCase, {onFalse: () => undefined, onTrue: () => 'i'})
155
155
  // bad
156
156
  const type = event.type === 'text-delta' ? 'text' : event.type === 'reasoning-delta' ? 'reasoning' : 'unknown'
157
157
  const flags = ignoreCase ? 'i' : undefined // "instead of a ternary the Boolean.match would be cleaner"
158
+ {props.children ? props.children(entry) : <Label entry={entry} />} // "the ternaries are impossible to read"
158
159
  // bad
159
160
  Array.appendAll(
160
161
  base,
@@ -182,6 +183,17 @@ if (Option.isSome(open)) open.value.append(event.delta)
182
183
  const parsed = Option.getOrUndefined(Number.parse(event.target.value))
183
184
  ```
184
185
 
186
+ **Absence.** `undefined` marks an absent value. Use `null` only where an outside contract, such as a stored schema or a library callback, demands it, with an inline disable that names the contract.
187
+
188
+ ```ts
189
+ // good
190
+ const last = Option.getOrUndefined(Array.last(sessions))
191
+ // oxlint-disable-next-line no-restricted-properties -- The schema stores a missing link as null.
192
+ url: Option.getOrNull(url)
193
+ // bad
194
+ failed: Option.getOrNull(Option.none()) // "i don't like null, i prefer undefined"
195
+ ```
196
+
185
197
  **Handlers.** A handler used once is an arrow in place; a handler used twice is a `function`.
186
198
 
187
199
  **Accumulators.** No `let`: a fold or an Effect collection operation that says what it selects.
@@ -209,6 +221,7 @@ items.filter(item => supports(item)).map(item => item.value)
209
221
  // good
210
222
  Number.parse(input.count)
211
223
  Predicate.isString(value)
224
+ Predicate.hasProperty(node, 'tagName')
212
225
  Predicate.isNotNullish(annotation)
213
226
  Array.isReadonlyArrayEmpty(node.arguments)
214
227
  String.isNonEmpty(event.delta)
@@ -223,6 +236,7 @@ const serve = Effect.gen(function* () {
223
236
  // bad
224
237
  parseInt(input.count)
225
238
  typeof value === 'string'
239
+ 'tagName' in node // "typeof and in shouldn't be allowed, probably this should use effect schema"
226
240
  annotation !== null && annotation !== undefined
227
241
  node.arguments.length === 0
228
242
  Effect.promise(() => new Promise(resolve => setTimeout(resolve, delay)))
@@ -306,11 +320,11 @@ files: ['packages/components-web/src/form.tsx', 'packages/components-web/src/ric
306
320
 
307
321
  ## Effect
308
322
 
309
- **Functions.** Use Effect.fn('Name.method') for a traced function with arguments and Effect.fnUntraced for an internal helper. Use Effect.gen for an effect with several statements and no arguments, and a single effect directly. Use Effect.fn.Return for an explicit return type.
323
+ **Functions.** Use Effect.fn('Name.method') for a traced function with arguments and Effect.fnUntraced for an internal helper. Use Effect.gen for an effect with several statements and no arguments, and a single effect directly. Use Effect.fn.Return only for a generator that calls itself.
310
324
 
311
325
  ```ts
312
326
  // good
313
- const prompt = Effect.fn('Ai.prompt')(function* (message: Prompt.UserMessage): Effect.fn.Return<void, AiError> {
327
+ const prompt = Effect.fn('Ai.prompt')(function* (message: Prompt.UserMessage) {
314
328
  const input = yield* piUserMessage(message)
315
329
  yield* send(input)
316
330
  })
@@ -405,15 +419,17 @@ const traced = Effect.withSpan('Notes.create')(notes.create(input))
405
419
  Effect.addFinalizer(() => closeSocket(socket)) // acquisition and release with different owners
406
420
  ```
407
421
 
408
- **Runtime.** The owned runtime runs the program. Bridge to a Promise once, where an external contract demands it. React is no such contract: atoms run its Effects.
422
+ **Runtime.** The entry point runs the program with `NodeRuntime.runMain`. Bridge to a Promise once, where an external contract demands it, with `Effect.runPromise` and an inline disable that names the contract. React is no such contract: atoms run its Effects.
409
423
 
410
424
  ```ts
411
425
  // good
412
426
  NodeRuntime.runMain(program)
413
- health: flow(rpc.health, Effect.runPromiseWith(Context.empty())),
427
+ // oxlint-disable-next-line no-restricted-properties -- Bingo's produce contract demands a Promise.
428
+ produce: flow(generate, Effect.runPromise),
414
429
  // bad
415
430
  const runtime = ManagedRuntime.make(AppLayer)
416
431
  const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromise(effect)
432
+ Effect.runPromiseWith(Context.empty()) // "what's the point of using the runPromiseWith if i pass an empty context?"
417
433
  ```
418
434
 
419
435
  **Sorting.** `Order` for sorting, `Result` for a fallible pure value.
@@ -431,7 +447,7 @@ entries.sort((left, right) => left.tokens - right.tokens)
431
447
 
432
448
  ## Types and Schema
433
449
 
434
- **Inference.** Infer types. Annotate recursion or branches TypeScript cannot unite; use schema types and indexed members in signatures. Keep intentional widening, not repeated inferred generics.
450
+ **Inference.** Infer types. Annotate a return type only on a type guard, an assertion function, a recursive function or the thunk of a recursive `Schema.suspend`. Check a returned object with `satisfies`, and type an empty or one-element array with `Array.empty<T>()` or `Array.of<T>(...)` instead of its callback. Use schema types and indexed members in signatures. Keep intentional widening, not repeated inferred generics.
435
451
 
436
452
  ```ts
437
453
  // good
@@ -444,8 +460,14 @@ function visit(node: Node): Result {
444
460
  function settle(amount: LedgerEntry['amount']) {
445
461
  return BigDecimal.format(amount)
446
462
  }
463
+ function still(bytes: Uint8Array) {
464
+ return {bytes, type: 'image/jpeg'} satisfies Image
465
+ }
447
466
  export type Sandbox = ReturnType<typeof fromSdk>
448
467
  // bad
468
+ function still(bytes: Uint8Array): Image {
469
+ return {bytes, type: 'image/jpeg'}
470
+ } // "not a fan of assigning the return type, i would have preferred a satisfies"
449
471
  const Input = Schema.Struct({value: Schema.String}) satisfies Schema.Schema<Input>
450
472
  const label = MutableRef.make<string>('') // repeats the inferred type
451
473
  export type SandboxLike = {id: string; renew: (seconds: number) => Promise<unknown>} // a copy of an inferred shape
@@ -513,7 +535,7 @@ function isPathNotFound(error: unknown) {
513
535
 
514
536
  ## Shape
515
537
 
516
- **Inline.** Inline forwarding wrappers, temporaries and constants. Keep a helper only for logic that is reused or deserves its own name. Argument-free functions are named values.
538
+ **Inline.** Inline forwarding wrappers, temporaries, constants and property aliases; keep an alias only where a callback needs its narrowed type. Inline a small helper with one caller. A helper that several files share lives in the package's `lib/utils`. Argument-free functions are named values.
517
539
 
518
540
  ```ts
519
541
  // good
@@ -530,6 +552,10 @@ function isBackendRequest(request: IncomingMessage) {
530
552
  } // "this is just a wrapper / factory function"
531
553
  Ref.set(entries, Array.copy(decoded)) // decoded is already an array
532
554
  const runPromise = Effect.runPromiseWith(Context.empty()) // "wrapper function, tmp variable ... that can be inlined"
555
+ const parent = read.parent // "inline the property access since it add no value"
556
+ function isVideo(type: FileType) {
557
+ return String.startsWith('video/')(type)
558
+ } // String.startsWith('video/') is already the function
533
559
  ```
534
560
 
535
561
  **Linked state.** Linked state moves in one pure updater, never a setter that calls another.
@@ -565,7 +591,7 @@ const warned = ['sort-keys', 'typescript/no-restricted-types'] // a package rule
565
591
 
566
592
  ## React
567
593
 
568
- **Components.** React Compiler memoizes. Destructure state at its declaration, receive component refs through props, and keep only a component's own interaction state in React.
594
+ **Components.** React Compiler memoizes. Import hooks by name from `react`. Destructure state at its declaration, receive component refs through props, and keep only a component's own interaction state in React.
569
595
 
570
596
  ```tsx
571
597
  // good
@@ -575,6 +601,7 @@ const moveRpc = useAtomSet(RpcClient.mutation('portfolio.move'))
575
601
  // bad
576
602
  const handle = useCallback(() => setOpen(true), [])
577
603
  const state = useState(false)
604
+ const [open, setOpen] = React.useState(false) // "why are you using the React.useState and not importing useState"
578
605
  ```
579
606
 
580
607
  **Atoms.** Data, shared state and every Effect a component needs live in atoms; a component reads them with hooks and never runs an Effect. A task that changes a React Query hook, a fetching Effect or an Effect runner in React code moves that code to an atom. Declare atoms at module level or in `Atom.family`, never during render. Import `Atom` and `AsyncResult` from `effect/reactivity` and the hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)`, and build every read and write with that runtime's `atom` and `fn`. Tie each write to the reads it changes with reactivity keys; only a runtime's `fn` honors them, so never pass them to `Atom.fn`.
@@ -42,7 +42,7 @@ assert.strictEqual(Array.flatMap(attioProvider.groups, group => group.actions).l
42
42
  - **Branches.** Exercise the reachable distinctions: empty, duplicate, concurrent and dependency failure. Write no Cartesian product, and no random or repeated happy paths without a hypothesis.
43
43
  - **Failure.** Use `Effect.flip` for an expected failure and `Effect.exit` for both outcomes. Assert domain identity and preserved state and resources: no partial write, lost value, extra retry or leak. Wording or rejection alone is not enough, and incidental order is no assertion.
44
44
  - **Control.** Run the regression test against the unfixed implementation, and confirm that the intended assertion fails, not setup, imports or compilation. Before a refactor, cover the old behavior.
45
- - **Audit.** Remove duplicate and tautological cases in the touched scope, but retain independent protocol, package, validation, authorization and persistence contracts. Mock call counts do not prove an external boundary. Do not replace a useful test with a disposable script just to reduce the count.
45
+ - **Audit.** Delete a case that no plausible wrong implementation fails, such as an assertion on a constant placeholder. Remove duplicate and tautological cases in the touched scope, but retain independent protocol, package, validation, authorization and persistence contracts. Mock call counts do not prove an external boundary. Do not replace a useful test with a disposable script just to reduce the count.
46
46
 
47
47
  ## Effect and fixtures
48
48