@starci/eslint-canon-fe 3.0.2 → 5.1.2
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 +35 -0
- package/README.md +41 -16
- package/brand-values.mjs +174 -0
- package/class-names.mjs +1 -1
- package/client-boundary.mjs +199 -0
- package/comments.mjs +144 -190
- package/docs/brand-values.md +30 -0
- package/docs/client-boundary.md +99 -0
- package/docs/e2e-shape.md +161 -0
- package/docs/env-owner.md +52 -0
- package/docs/formatting.md +30 -0
- package/docs/hooks-folder.md +29 -0
- package/docs/hygiene.md +111 -0
- package/docs/lint-escape-hatch.md +32 -0
- package/docs/lists.md +74 -0
- package/docs/native-controls.md +73 -0
- package/docs/next-conventions.md +206 -0
- package/docs/size-and-state-budget.md +73 -0
- package/docs/spec-quality.md +139 -0
- package/docs/translation.md +50 -0
- package/docs/transport.md +168 -0
- package/docs/type-safety.md +95 -0
- package/e2e-shape.mjs +348 -0
- package/env-owner.mjs +134 -0
- package/file-layout.mjs +100 -7
- package/formatting.mjs +106 -0
- package/hooks-folder.mjs +119 -0
- package/hygiene.mjs +235 -0
- package/icon.mjs +210 -211
- package/index.mjs +292 -131
- package/lib/ast.mjs +94 -0
- package/lib/path.mjs +2 -0
- package/lib/scope.mjs +42 -0
- package/lib/why.mjs +304 -0
- package/lint-adoption.mjs +15 -52
- package/lint-escape-hatch.mjs +91 -56
- package/lists.mjs +197 -0
- package/loading.mjs +163 -164
- package/naming.mjs +143 -143
- package/native-controls.mjs +112 -0
- package/next-conventions.mjs +346 -0
- package/package.json +7 -3
- package/props-and-slots.mjs +4 -2
- package/served-locale.mjs +160 -160
- package/shape-slot.mjs +339 -0
- package/size-and-state-budget.mjs +196 -0
- package/spec-quality.mjs +239 -0
- package/the-split.mjs +133 -134
- package/tokens.mjs +267 -268
- package/translation.mjs +262 -122
- package/transport.mjs +404 -0
- package/type-safety.mjs +156 -74
- package/typography.mjs +82 -83
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Spec quality
|
|
2
|
+
|
|
3
|
+
Law module: `spec-quality.mjs`. Catalogue: R67 FE_SPEC_QUALITY, R60 (real catalogue in specs).
|
|
4
|
+
|
|
5
|
+
A spec is a claim about one unit's behaviour: no class-string pinning, it imports the unit beside it, no spec for a barrel, no `as unknown as`, an axe assertion for every connected screen, and a real `next-intl` catalogue instead of a mock.
|
|
6
|
+
|
|
7
|
+
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
|
+
|
|
9
|
+
## `starci-fe/no-class-string-in-spec`
|
|
10
|
+
|
|
11
|
+
A component spec asserts behaviour, never a class string.
|
|
12
|
+
|
|
13
|
+
**Invalid** (`src/components/leaves/Chip/Chip.test.tsx`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
expect(el).toHaveClass("bg-primary")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Valid** (`src/components/leaves/Chip/Chip.test.tsx`)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
expect(screen.getByRole("button", { name: "Save" })).toBeEnabled()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Finding code:** `FE_SPEC_QUALITY`
|
|
26
|
+
|
|
27
|
+
**Vì sao (why):** Spec `<file>` ghim chuỗi class. Spec kiểm hành vi, không kiểm stylesheet.
|
|
28
|
+
|
|
29
|
+
**Cách sửa:** Assert theo role, tên, trạng thái hoặc chữ hiển thị.
|
|
30
|
+
|
|
31
|
+
## `starci-fe/spec-tests-its-neighbour`
|
|
32
|
+
|
|
33
|
+
A spec imports its sibling subject: `X.test.tsx` imports `./X`.
|
|
34
|
+
|
|
35
|
+
**Invalid** (`src/components/leaves/Chip/Chip.test.tsx`)
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Row } from "../Row"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Valid** (`src/components/leaves/Chip/Chip.test.tsx`)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Chip } from "./Chip"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Finding code:** `FE_SPEC_QUALITY`
|
|
48
|
+
|
|
49
|
+
**Vì sao (why):** Spec `<file>` không import chủ thể nằm cạnh nó.
|
|
50
|
+
|
|
51
|
+
**Cách sửa:** Đưa spec về cạnh chủ thể, hoặc test đúng đơn vị nằm cạnh.
|
|
52
|
+
|
|
53
|
+
## `starci-fe/no-barrel-spec`
|
|
54
|
+
|
|
55
|
+
No `index.test.ts` beside an `index.ts` barrel.
|
|
56
|
+
|
|
57
|
+
**Invalid** (`src/hooks/index.test.ts`)
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// file: src/hooks/index.test.ts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Valid** (`src/hooks/feed/useFeed.test.ts`)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// file: src/hooks/feed/useFeed.test.ts
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Finding code:** `FE_SPEC_QUALITY`
|
|
70
|
+
|
|
71
|
+
**Vì sao (why):** Spec `<file>` đặt cạnh một barrel. Barrel không có hành vi để test.
|
|
72
|
+
|
|
73
|
+
**Cách sửa:** Xóa spec; test từng đơn vị mà barrel xuất lại, cạnh chính đơn vị đó.
|
|
74
|
+
|
|
75
|
+
## `starci-fe/no-double-cast-in-spec`
|
|
76
|
+
|
|
77
|
+
A spec builds its values with the type's own shape, not `as unknown as`.
|
|
78
|
+
|
|
79
|
+
**Invalid** (`any spec`)
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const x = { id: 1 } as unknown as Course
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Valid** (`any spec`)
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const x = mock<Course>()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Finding code:** `FE_SPEC_QUALITY`
|
|
92
|
+
|
|
93
|
+
**Vì sao (why):** Spec `<file>` ép kiểu hai lần (`as unknown as`).
|
|
94
|
+
|
|
95
|
+
**Cách sửa:** Dựng giá trị bằng factory có kiểu (`mock<T>()` hoặc fixture builder).
|
|
96
|
+
|
|
97
|
+
## `starci-fe/connected-spec-has-axe`
|
|
98
|
+
|
|
99
|
+
The spec of a connected block or feature runs an axe assertion.
|
|
100
|
+
|
|
101
|
+
**Invalid** (`src/components/blocks/Feed/index.test.tsx`)
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
it("renders", () => {})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Valid** (`src/components/blocks/Feed/index.test.tsx`)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
it("a11y", async () => { expect(await axe(container)).toHaveNoViolations() })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Finding code:** `FE_SPEC_QUALITY`
|
|
114
|
+
|
|
115
|
+
**Vì sao (why):** Spec `<file>` của màn hình kết nối không có assert axe.
|
|
116
|
+
|
|
117
|
+
**Cách sửa:** Thêm `expect(await axe(container)).toHaveNoViolations()`.
|
|
118
|
+
|
|
119
|
+
## `starci-fe/no-mocked-translations`
|
|
120
|
+
|
|
121
|
+
A spec renders with the real catalogue; it never mocks `next-intl`.
|
|
122
|
+
|
|
123
|
+
**Invalid** (`any spec`)
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
vi.mock("next-intl", () => ({}))
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Valid** (`any spec`)
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
render(<NextIntlClientProvider messages={messages} locale="vi" />)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Finding code:** `FE_I18N_CATALOG`
|
|
136
|
+
|
|
137
|
+
**Vì sao (why):** Spec `<file>` giả lập `next-intl`, nên chỉ kiểm khóa chứ không kiểm catalog thật.
|
|
138
|
+
|
|
139
|
+
**Cách sửa:** Render trong `NextIntlClientProvider` với `messages/<locale>.json` thật.
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
```
|
|
37
|
+
|
|
38
|
+
**Valid** (`src/components/blocks/Feed/index.tsx`)
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
<button>{t("save")}</button>
|
|
42
|
+
<input placeholder={t("search")} />
|
|
43
|
+
const o = { title: t("courses.title") }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Finding code:** `FE_I18N_LITERAL`
|
|
47
|
+
|
|
48
|
+
**Vì sao (why):** Chữ `"<text>"` ở `<file>:<line>` không đi qua `t()`.
|
|
49
|
+
|
|
50
|
+
**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,168 @@
|
|
|
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 app, one client (`modules/api/client.ts`), one result vocabulary (`Outcome<T>`: `ok`, `refused`, `invalid`, `not-found`, `unavailable`). 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 app's single `fetch` lives in `modules/api/client.ts`.
|
|
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 thư viện HTTP khác) ở `<file>` nằm ngoài `modules/api/client.ts`. Mỗi app chỉ có đúng một đường truyền.
|
|
28
|
+
|
|
29
|
+
**Cách sửa:** Gọi client của app 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
|
+
`modules/api/client.ts` handles 401 and 403 and produces `refused`.
|
|
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 { kind: "refused" }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Finding code:** `FE_HTTP_STATUS_COLLAPSE`
|
|
93
|
+
|
|
94
|
+
**Vì sao (why):** Client ở `<file>` không đổi 401/403 thành `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 401/403 trả `{ kind: "refused" }` 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
|
+
Wire types come from the contract copy via codegen; responses are not cast.
|
|
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
|
+
```
|
|
135
|
+
|
|
136
|
+
**Finding code:** `FE_WIRE_GENERATED`
|
|
137
|
+
|
|
138
|
+
**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/`.
|
|
139
|
+
|
|
140
|
+
**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`.
|
|
141
|
+
|
|
142
|
+
## `starci-fe/outcome-kinds-exhaustive`
|
|
143
|
+
|
|
144
|
+
A `switch` over an Outcome's `kind` has a case for every kind (catalogue R51, sub-check `FE_OUTCOME_KIND_UNHANDLED`).
|
|
145
|
+
|
|
146
|
+
**Invalid** (`src/hooks/course/useCourse.ts`)
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
switch (outcome.kind) { case "ok": return outcome.data; default: return null }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Valid** (`src/hooks/course/useCourse.ts`)
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
switch (outcome.kind) {
|
|
156
|
+
case "ok": return outcome.data
|
|
157
|
+
case "refused": return goToSignIn()
|
|
158
|
+
case "invalid": return showIssues(outcome.issues)
|
|
159
|
+
case "not-found": return notFoundState()
|
|
160
|
+
case "unavailable": return retryState(outcome.retryable)
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Finding code:** `FE_OUTCOME_KIND_UNHANDLED`
|
|
165
|
+
|
|
166
|
+
**Vì sao (why):** `switch` trên `kind` ở `<file>` có nhánh `ok` nhưng thiếu một trong refused, invalid, not-found, unavailable.
|
|
167
|
+
|
|
168
|
+
**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`.
|
|
@@ -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.
|