@starci/eslint-canon-fe 4.0.0 → 6.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/CHANGELOG.md +111 -0
- package/README.md +33 -16
- package/brand-values.mjs +172 -0
- package/class-names.mjs +9 -9
- package/client-boundary.mjs +287 -0
- package/comments.mjs +137 -190
- package/docs/brand-values.md +30 -0
- package/docs/client-boundary.md +124 -0
- package/docs/e2e-shape.md +161 -0
- package/docs/env-owner.md +52 -0
- package/docs/formatting.md +30 -0
- package/docs/grammar-boundary.md +33 -0
- package/docs/hooks-folder.md +29 -0
- package/docs/hygiene.md +122 -0
- package/docs/lint-escape-hatch.md +34 -0
- package/docs/lists.md +74 -0
- package/docs/native-controls.md +73 -0
- package/docs/next-conventions.md +232 -0
- package/docs/size-and-state-budget.md +73 -0
- package/docs/size-growth.md +29 -0
- package/docs/spec-quality.md +147 -0
- package/docs/status-colors.md +32 -0
- package/docs/translation.md +54 -0
- package/docs/transport.md +195 -0
- package/docs/type-safety.md +95 -0
- package/e2e-shape.mjs +345 -0
- package/env-owner.mjs +134 -0
- package/file-layout.mjs +105 -104
- package/formatting.mjs +104 -0
- package/grammar-boundary.mjs +85 -9
- package/hooks-folder.mjs +120 -0
- package/hygiene.mjs +513 -0
- package/icon.mjs +212 -212
- package/index.mjs +243 -142
- package/landmark.mjs +0 -11
- package/lib/ast.mjs +94 -0
- package/lib/bindings.mjs +144 -0
- package/lib/config.mjs +79 -0
- package/lib/hfs.mjs +48 -0
- package/lib/next.mjs +6 -0
- package/lib/path.mjs +2 -0
- package/lib/platform.mjs +66 -0
- package/lib/react.mjs +28 -0
- package/lib/scope.mjs +142 -0
- package/lib/status-tones.generated.mjs +9 -0
- package/lib/types.mjs +63 -0
- package/lib/why.mjs +334 -0
- package/lint-escape-hatch.mjs +93 -56
- package/lists.mjs +197 -0
- package/loading.mjs +156 -164
- package/naming.mjs +149 -143
- package/native-controls.mjs +112 -0
- package/next-conventions.mjs +430 -0
- package/package.json +15 -6
- package/props-and-slots.mjs +19 -13
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/engine/runtime-root.mjs +32 -0
- package/runtime/engine/yaml.mjs +161 -0
- package/runtime/knowledge/hfs/slots.yaml +1072 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/glob.mjs +23 -0
- package/runtime/scripts/lib/hfs-allows.mjs +42 -0
- package/runtime/scripts/lib/hfs-slots.mjs +722 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/next-contract.mjs +14 -0
- package/runtime/scripts/lib/path-key.mjs +17 -0
- package/runtime/scripts/lib/recorded-lines.mjs +27 -0
- package/served-locale.mjs +174 -160
- package/shape-slot.mjs +22 -21
- package/size-and-state-budget.mjs +214 -0
- package/size-growth.mjs +58 -0
- package/spec-quality.mjs +318 -0
- package/status-colors.mjs +139 -0
- package/the-split.mjs +133 -134
- package/tokens.mjs +259 -268
- package/translation.mjs +295 -122
- package/transport.mjs +752 -0
- package/type-safety.mjs +147 -74
- package/typography.mjs +78 -83
- package/vendor-boundary.mjs +5 -27
- package/lint-adoption.mjs +0 -116
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Translation: no literal copy, no pragma
|
|
2
|
+
|
|
3
|
+
Law module: `translation.mjs`. Catalogue: R58 FE_I18N_LITERAL.
|
|
4
|
+
|
|
5
|
+
No text a reader can see or hear is written in source, at any tier and in any language: it is read from a `next-intl` catalogue through `t()`. There is no `vn-ok` pragma, no endonym exemption and no `resources/` copy folder; the catalogues (`messages/<locale>.json`), fixtures and specs are the only content paths. The notion of a literal is the same as nivo-fe's `scripts/check-i18n-catalog.mjs`: a word is two or more letters, and the copy attributes are `label`, `title`, `alt`, `placeholder` and the `aria-*` set.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/no-copy-resolution-below-block`
|
|
10
|
+
|
|
11
|
+
The vocabulary tiers receive resolved strings; they never resolve one.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/components/leaves/Input/index.tsx`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const t = useTranslations("input")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Valid** (`src/components/leaves/Input/index.tsx`)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const E = ({ props }) => props.label
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## `starci-fe/no-hardcoded-copy`
|
|
27
|
+
|
|
28
|
+
User-facing text comes from a `next-intl` catalogue through `t()`, at every tier.
|
|
29
|
+
|
|
30
|
+
**Invalid** (`src/components/blocks/Feed/index.tsx`)
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
<button>Save</button>
|
|
34
|
+
<input placeholder="Search courses" />
|
|
35
|
+
const o = { title: "Your courses" }
|
|
36
|
+
const status = { ready: "Your course is ready" }
|
|
37
|
+
<p>{`${count} installed`}</p>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Valid** (`src/components/blocks/Feed/index.tsx`)
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
<button>{t("save")}</button>
|
|
44
|
+
<input placeholder={t("search")} />
|
|
45
|
+
const o = { title: t("courses.title") }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A template literal with substitutions is copy when its static parts contain a word (`${count} installed`, `You have ${c} unread messages`) in a JSX child, a copy attribute or a copy-key value; class names, URLs, keys, ids, units and format tokens (`btn-${tone}`, `/courses/${id}`, `${n}px`, `YYYY-MM-DD HH:mm`) are not copy. An object property under any key is copy when its value is a whole sentence (two or more words, starting like a sentence, ending like one, or in a non-ASCII script): a hook returning `{ ready: "Your course is ready" }` is a finding.
|
|
49
|
+
|
|
50
|
+
**Finding code:** `FE_I18N_LITERAL`
|
|
51
|
+
|
|
52
|
+
**Vì sao (why):** Chữ `"<text>"` ở `<file>:<line>` không đi qua `t()`.
|
|
53
|
+
|
|
54
|
+
**Cách sửa:** Chuyển câu vào `modules/i18n/messages/<locale>.json` và đọc bằng `t("khóa")`; không có ngoại lệ và không có `vn-ok`.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Transport, status and wire
|
|
2
|
+
|
|
3
|
+
Law module: `transport.mjs`. Catalogue: R50 FE_TRANSPORT_OWNER, R51 FE_HTTP_STATUS_COLLAPSE, R52 FE_WIRE_GENERATED.
|
|
4
|
+
|
|
5
|
+
One repository, one client, one result vocabulary. The client is the file of HFS slot `fe.transport.client` (`apps/<app>/src/modules/api/client.ts`, a one-app repository) or `fe.package.api.client` (`packages/<family>-api/src/client.ts`, the shared client of a multi-app repository); the vocabulary is `Outcome<T>` (`ok`, `refused`, `invalid`, `not-found`, `unavailable`), declared in the `fe.transport.outcome` / `fe.package.api.outcome` file. The rules read the slot of the file being linted, never its path. 401 and 403 become `refused`; nothing collapses a status into `null`; wire types are generated from the contract copy in `modules/api/contract/`.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/fetch-only-in-api-client`
|
|
10
|
+
|
|
11
|
+
The repository's single `fetch` (and `Request`, `EventSource`, `navigator.sendBeacon`) lives in the api client slot. The global is found by scope resolution: `fetch(...)`, `globalThis.fetch`, `window.fetch`, `self.fetch`, `const f = fetch` and `const { fetch } = globalThis` all count, a local or imported `fetch` does not. A fetch library is refused by module specifier (axios, ky, got, undici, ofetch, graphql-request, urql, `@apollo/*`, ...; `import`, `export ... from`, `import()` and `require`), also inside the client. Another file of the api package (`src/transport.ts`) is not the client.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/hooks/course/useCourse.ts`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const r = await fetch(url)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Valid** (`src/hooks/course/useCourse.ts`)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const r = await client.get(url)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Finding code:** `FE_TRANSPORT_OWNER`
|
|
26
|
+
|
|
27
|
+
**Vì sao (why):** `fetch` (hoặc `Request`, `EventSource`, `sendBeacon`, thư viện HTTP khác) ở `<file>` nằm ngoài client duy nhất (`modules/api/client.ts`, hoặc `src/client.ts` của gói api dùng chung). Mỗi repo chỉ có đúng một đường truyền.
|
|
28
|
+
|
|
29
|
+
**Cách sửa:** Gọi client của repo và nhận `Outcome<T>`; không tự gọi `fetch`.
|
|
30
|
+
|
|
31
|
+
## `starci-fe/client-fetch-has-signal`
|
|
32
|
+
|
|
33
|
+
Every `fetch` in `modules/api/client.ts` passes an AbortSignal (timeout or caller).
|
|
34
|
+
|
|
35
|
+
**Invalid** (`src/modules/api/client.ts`)
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
fetch(url, { method: "GET" })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Valid** (`src/modules/api/client.ts`)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
fetch(url, { method: "GET", signal: AbortSignal.timeout(8000) })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Finding code:** `FE_TRANSPORT_OWNER`
|
|
48
|
+
|
|
49
|
+
**Vì sao (why):** `fetch` ở `<file>` không có `signal`. Client bắt buộc có timeout và `AbortSignal`.
|
|
50
|
+
|
|
51
|
+
**Cách sửa:** Truyền `signal` (`AbortSignal.timeout(...)` kết hợp với tín hiệu của người gọi).
|
|
52
|
+
|
|
53
|
+
## `starci-fe/no-shared-transport-state`
|
|
54
|
+
|
|
55
|
+
`modules/api/**` holds no module-level `let`/`var`.
|
|
56
|
+
|
|
57
|
+
**Invalid** (`src/modules/api/client.ts`)
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
let token = null
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Valid** (`src/modules/api/client.ts`)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export const request = (token: string) => token
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Finding code:** `FE_TRANSPORT_OWNER`
|
|
70
|
+
|
|
71
|
+
**Vì sao (why):** `<file>` giữ trạng thái dùng chung dạng `let`/`var` trong tầng API. Token và locale không được nằm trong singleton.
|
|
72
|
+
|
|
73
|
+
**Cách sửa:** Truyền credential và locale bằng tham số hoặc context.
|
|
74
|
+
|
|
75
|
+
## `starci-fe/client-maps-auth-to-refused`
|
|
76
|
+
|
|
77
|
+
The api client branches on the response status: a comparison of the fetch response's status (typed as a `Response`) with 401 and 403 (`===`, `||`, a literal list `.includes`/`.has`, or a `switch` with both cases) whose consequent builds an object with `kind: "refused"`. The three literals lying anywhere in the file do not pass, and neither does a mapping delegated to a function in another file.
|
|
78
|
+
|
|
79
|
+
**Invalid** (`src/modules/api/client.ts`)
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const r = await fetch(u, { signal })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Valid** (`src/modules/api/client.ts`)
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const r = await fetch(u, { signal })
|
|
89
|
+
if (r.status === 401 || r.status === 403) return { ok: false, kind: "refused" }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Finding code:** `FE_HTTP_STATUS_COLLAPSE`
|
|
93
|
+
|
|
94
|
+
**Vì sao (why):** Client ở `<file>` không có nhánh so sánh `response.status` với 401 và 403 rồi trả `{ kind: "refused" }`, nên trạng thái "cần đăng nhập" không bao giờ đạt được.
|
|
95
|
+
|
|
96
|
+
**Cách sửa:** Thêm nhánh `response.status === 401 || response.status === 403` trả `{ ok: false, kind: "refused" }` ngay trong client.
|
|
97
|
+
|
|
98
|
+
## `starci-fe/no-http-status-collapse`
|
|
99
|
+
|
|
100
|
+
A non-ok response is not folded into one branch or into null; 401/403 become `refused`.
|
|
101
|
+
|
|
102
|
+
**Invalid** (`src/modules/api/client.ts`)
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
if (!res.ok) return null
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Valid** (`src/modules/api/client.ts`)
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
if (!res.ok) return toOutcome(res.status)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Finding code:** `FE_HTTP_STATUS_COLLAPSE`
|
|
115
|
+
|
|
116
|
+
**Vì sao (why):** `<file>` gộp mọi mã HTTP thành một nhánh (hoặc trả null khi phản hồi lỗi). 401/403 phải thành `refused`.
|
|
117
|
+
|
|
118
|
+
**Cách sửa:** Trả `Outcome` theo mã: `refused` (401/403), `not-found`, `invalid`, `unavailable`; không trả nguyên văn lỗi của server làm lý do.
|
|
119
|
+
|
|
120
|
+
## `starci-fe/no-hand-typed-wire`
|
|
121
|
+
|
|
122
|
+
A transport response body is narrowed from `unknown` or typed by a generated wire type (a type imported from a `__generated__/` module), never by a hand-written one: `(await response.json()) as unknown` is the sanctioned entry and does not fire; an assertion or annotation of a `.json()` read (directly, or through a `const` initialised with one) to any other type does. Inline GraphQL documents are also refused. A hand-declared interface is not judged by its name; only a response read typed by hand is.
|
|
123
|
+
|
|
124
|
+
**Invalid** (`src/modules/api/course/read-course.ts`)
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const x = (await res.json()) as Course
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Valid** (`src/modules/api/course/read-course.ts`)
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import type { CourseQuery } from "../__generated__/graphql"
|
|
134
|
+
const raw = (await res.json()) as unknown
|
|
135
|
+
const x: CourseQuery = parse(raw)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Finding code:** `FE_WIRE_GENERATED`
|
|
139
|
+
|
|
140
|
+
**Vì sao (why):** `<file>` tự gõ kiểu wire hoặc ép kiểu phản hồi. Dùng kiểu sinh từ `contract/`.
|
|
141
|
+
|
|
142
|
+
**Cách sửa:** Chạy codegen từ bản sao hợp đồng ở `modules/api/contract/` và nhập kiểu sinh ra; đưa tài liệu GraphQL vào tệp `.graphql`.
|
|
143
|
+
|
|
144
|
+
## `starci-fe/outcome-kinds-exhaustive`
|
|
145
|
+
|
|
146
|
+
A `switch` over an Outcome's `kind` has a case for every kind (catalogue R51, sub-check `FE_OUTCOME_KIND_UNHANDLED`).
|
|
147
|
+
|
|
148
|
+
**Invalid** (`src/hooks/course/useCourse.ts`)
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
switch (outcome.kind) { case "ok": return outcome.data; default: return null }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Valid** (`src/hooks/course/useCourse.ts`)
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
switch (outcome.kind) {
|
|
158
|
+
case "ok": return outcome.data
|
|
159
|
+
case "refused": return goToSignIn()
|
|
160
|
+
case "invalid": return showIssues(outcome.issues)
|
|
161
|
+
case "not-found": return notFoundState()
|
|
162
|
+
case "unavailable": return retryState(outcome.retryable)
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Finding code:** `FE_OUTCOME_KIND_UNHANDLED`
|
|
167
|
+
|
|
168
|
+
**Vì sao (why):** `switch` trên `kind` ở `<file>` có nhánh `ok` nhưng thiếu một trong refused, invalid, not-found, unavailable.
|
|
169
|
+
|
|
170
|
+
**Cách sửa:** Viết đủ năm nhánh của `Outcome<T>`, mỗi nhánh một màn; không dựa vào `default`.
|
|
171
|
+
|
|
172
|
+
## `starci-fe/one-outcome-union`
|
|
173
|
+
|
|
174
|
+
A result union (`ok` / `kind` discriminant) is declared only in the outcome slot; elsewhere the code composes `Outcome<T>`.
|
|
175
|
+
|
|
176
|
+
A union type alias is a result union when its resolved members are all object types that carry the same literal-typed discriminant in the result vocabulary: `ok` (each member pins it to one value, `true` and `false` both present) or `kind` (at least one literal of the Outcome vocabulary: `ok`, `refused`, `forbidden`, `invalid`, `not-found`, `unavailable`). The members are read through the type checker, so `Ok<T> | Failure` and intersections are seen as what they are; no name (`*Outcome`, `*Result`) decides anything. UI state unions (`status`, `state`, `type`, a `kind` of tree nodes or menu items) are not result vocabulary and pass; so does an alias that only composes the one union (`type Read = Outcome<Course>`, `Exclude<Outcome<T>, ...>`). `Outcome<T> | { ok: false; kind: "conflict" }` adds an arm to the one union and is refused.
|
|
177
|
+
|
|
178
|
+
**Invalid** (`src/hooks/invite/useInvite.ts`)
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
export type InviteOutcome = { ok: true } | { ok: false; kind: "refused" }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Valid** (`src/hooks/invite/useInvite.ts`)
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import type { Outcome } from "@/modules/api/outcome"
|
|
188
|
+
export type InviteOutcome = Outcome<{ id: string }>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Finding code:** `FE_HTTP_STATUS_COLLAPSE`
|
|
192
|
+
|
|
193
|
+
**Vì sao (why):** `<file>` khai báo thêm một union kết quả (`ok` hoặc `kind`) ngoài `outcome.ts`. Repo chỉ có một `Outcome<T>`.
|
|
194
|
+
|
|
195
|
+
**Cách sửa:** Dùng `Outcome<T>` của `modules/api/outcome.ts` (hoặc gói api dùng chung); thêm chi tiết nghiệp vụ qua tham số thứ hai thay vì khai báo union mới.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Type safety
|
|
2
|
+
|
|
3
|
+
Law module: `type-safety.mjs`. Catalogue: R22 HFS_TS_STRICT, sub-check `FE_TYPE_ESCAPE`.
|
|
4
|
+
|
|
5
|
+
The compiler is told the truth or it is not told at all. The double cast, the plain assertion, the non-null assertion and `any` are the four spellings of "trust me". `as const`, `as unknown` and `satisfies` stay allowed. Specs are exempt: a test builds values the types forbid, because that is what it proves.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/no-double-cast`
|
|
10
|
+
|
|
11
|
+
No cast through `unknown`; it turns checking off at the boundary.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/modules/api/read-course.ts`)
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
const row = payload as unknown as CourseRow
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Valid** (`src/modules/api/read-course.ts`)
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const row = parseCourseRow(payload)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Finding code:** `FE_TYPE_ESCAPE`
|
|
26
|
+
|
|
27
|
+
**Vì sao (why):** `<file>` ép kiểu qua `unknown` (`as unknown as T`): trình biên dịch quên mọi thứ nó biết ngay tại chỗ dữ liệu đi vào.
|
|
28
|
+
|
|
29
|
+
**Cách sửa:** Thu hẹp từ `unknown` bằng type guard hoặc parser; không ép kiểu.
|
|
30
|
+
|
|
31
|
+
## `starci-fe/no-type-assertion`
|
|
32
|
+
|
|
33
|
+
No `as T` or `<T>x`; narrow with a check, a guard or a parser.
|
|
34
|
+
|
|
35
|
+
**Invalid** (`src/modules/api/read-course.ts`)
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
const row = payload as CourseRow
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Valid** (`src/modules/api/read-course.ts`)
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
const row = isCourseRow(payload) ? payload : null
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Finding code:** `FE_TYPE_ESCAPE`
|
|
48
|
+
|
|
49
|
+
**Vì sao (why):** `<file>` dùng `as T` hoặc `<T>x`: một lời khẳng định trình biên dịch không kiểm được. Sai thì lỗi rơi vào trình duyệt của người dùng.
|
|
50
|
+
|
|
51
|
+
**Cách sửa:** Thu hẹp bằng type guard, `in`, discriminant hoặc parser tại nơi dữ liệu đi vào; dùng `satisfies` nếu chỉ muốn kiểm một literal.
|
|
52
|
+
|
|
53
|
+
## `starci-fe/no-non-null-assertion`
|
|
54
|
+
|
|
55
|
+
No non-null assertion (`x!`); prove the value is there.
|
|
56
|
+
|
|
57
|
+
**Invalid** (`src/components/blocks/Feed/index.tsx`)
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
const first = rows[0]!
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Valid** (`src/components/blocks/Feed/index.tsx`)
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
const first = rows[0] ?? fallbackRow
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Finding code:** `FE_TYPE_ESCAPE`
|
|
70
|
+
|
|
71
|
+
**Vì sao (why):** `<file>` dùng `x!`: khẳng định giá trị có mặt mà không chứng minh.
|
|
72
|
+
|
|
73
|
+
**Cách sửa:** Xử lý nhánh vắng mặt (`if`, `??`, return sớm) hoặc sửa kiểu để giá trị không thể vắng.
|
|
74
|
+
|
|
75
|
+
## `starci-fe/no-explicit-any`
|
|
76
|
+
|
|
77
|
+
No `any`; use a real type or `unknown` and narrow.
|
|
78
|
+
|
|
79
|
+
**Invalid** (`src/modules/api/read-course.ts`)
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
const parse = (value: any) => value.id
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Valid** (`src/modules/api/read-course.ts`)
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const parse = (value: unknown) => (isCourseRow(value) ? value.id : null)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Finding code:** `FE_TYPE_ESCAPE`
|
|
92
|
+
|
|
93
|
+
**Vì sao (why):** `<file>` dùng `any`: tắt kiểm kiểu cho giá trị đó và mọi thứ suy ra từ nó.
|
|
94
|
+
|
|
95
|
+
**Cách sửa:** Dùng kiểu thật, generic, hoặc `unknown` rồi thu hẹp tại chỗ dùng.
|