mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-10 05:10:58 +00:00
chore(lint): forbid the Number-or-clamp idiom in direct-write settings pages (#6129)
* chore(lint): forbid the Number-or-clamp idiom in direct-write settings pages Follow-up promised in #6127's review thread: the settings and xray pages write numeric changes straight into state, so a regressed handler silently ships the cleared-port bug again. A scoped no-restricted-syntax rule now rejects Number(...) || N inside an onChange attribute in those directories, pointing at onNumber(). The one remaining match, the Telegram notify interval, moves onto the helper with its floor intact: clearing now keeps the stored count instead of writing 1, and Math.max still clamps typed values. Form modals that stage values behind Zod keep their deliberate clear-means-zero semantics; the rule deliberately does not apply there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(lint): widen the numeric-clamp guard to the shapes that actually drift From review: the rule matched only the Number-or-literal shape, while two semantically identical ternary sites already lived inside its own directories, so 'zero suppressions' reflected the selector's narrowness rather than a clean subtree. The rule now catches the ternary typeof form and the nullish-coalescing form too, is anchored to InputNumber elements so its message can never point a ChangeEvent handler at a number-typed helper, and documents the extracted-handler shape it cannot see. The xray form modals stage values behind Zod like the clients modals do, so a follow-up config object exempts them explicitly instead of the comment claiming they were never in scope. BasicsTab's Happy Eyeballs try-delay — the one genuine direct-write ternary — moves onto onNumber: clearing keeps the stored delay instead of writing 0, and 0 stays reachable by typing it. The Telegram interval gains precision={0} so a typed decimal cannot compose an @every value its own parser rejects on reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,4 +53,37 @@ export default [
|
|||||||
'jsx-a11y/no-autofocus': 'off',
|
'jsx-a11y/no-autofocus': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// The settings and xray pages write numeric InputNumber changes straight
|
||||||
|
// into state, so a null-collapsing handler (`Number(v) || N`, or the
|
||||||
|
// ternary `typeof v === 'number' ? v : N`) turns a cleared field into a
|
||||||
|
// stored N — the cleared-port bug, #6121. Handlers here go through
|
||||||
|
// onNumber() (src/utils/onNumber.ts) instead. Known limit: a handler
|
||||||
|
// extracted into a variable and passed as onChange={handler} is not
|
||||||
|
// matched; the inline shapes below are the ones that drift in practice.
|
||||||
|
files: ['src/pages/settings/**/*.tsx', 'src/pages/xray/**/*.tsx'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-syntax': ['error', {
|
||||||
|
selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] LogicalExpression[operator="||"] > CallExpression[callee.name="Number"]',
|
||||||
|
message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
|
||||||
|
}, {
|
||||||
|
selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] ConditionalExpression[test.left.operator="typeof"][alternate.type="Literal"]',
|
||||||
|
message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
|
||||||
|
}, {
|
||||||
|
selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] LogicalExpression[operator="??"][right.type="Literal"]',
|
||||||
|
message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The xray form modals (OutboundFormModal, BalancerFormModal,
|
||||||
|
// DnsServerModal, WarpModal, …) stage values behind Zod validation like
|
||||||
|
// the clients/inbounds modals do, and some of their fields carry a
|
||||||
|
// deliberate clear-means-zero semantic — the direct-write rule above
|
||||||
|
// does not apply to them.
|
||||||
|
files: ['src/pages/xray/**/*Modal.tsx'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-syntax': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Alert, Button, Input, InputNumber, Select, Space, Switch, Tabs } from '
|
|||||||
import { BellOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
import { BellOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import { LanguageManager } from '@/utils';
|
import { LanguageManager } from '@/utils';
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil } from '@/utils';
|
||||||
|
import { onNumber } from '@/utils/onNumber';
|
||||||
import type { AllSetting } from '@/models/setting';
|
import type { AllSetting } from '@/models/setting';
|
||||||
import { SettingListItem } from '@/components/ui';
|
import { SettingListItem } from '@/components/ui';
|
||||||
import { TelegramNotifications } from '@/components/ui/notifications/TelegramNotifications';
|
import { TelegramNotifications } from '@/components/ui/notifications/TelegramNotifications';
|
||||||
@@ -122,9 +123,10 @@ function NotifyTimeField({ value, onChange }: { value: string; onChange: (v: str
|
|||||||
<Space.Compact style={{ width: '100%' }}>
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
min={1}
|
min={1}
|
||||||
|
precision={0}
|
||||||
style={{ width: '50%' }}
|
style={{ width: '50%' }}
|
||||||
value={state.num}
|
value={state.num}
|
||||||
onChange={(v) => update({ num: Math.max(1, Number(v) || 1) })}
|
onChange={onNumber((v) => update({ num: Math.max(1, v) }))}
|
||||||
aria-label={t('pages.settings.notifyTime.interval')}
|
aria-label={t('pages.settings.notifyTime.interval')}
|
||||||
/>
|
/>
|
||||||
<Select<Unit>
|
<Select<Unit>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { onNumber } from '@/utils/onNumber';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Alert, Button, Input, InputNumber, Modal, Select, Space, Switch, Tabs } from 'antd';
|
import { Alert, Button, Input, InputNumber, Modal, Select, Space, Switch, Tabs } from 'antd';
|
||||||
import {
|
import {
|
||||||
@@ -216,10 +217,10 @@ export default function BasicsTab({
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
value={directHappyEyeballs.tryDelayMs}
|
value={directHappyEyeballs.tryDelayMs}
|
||||||
placeholder="150"
|
placeholder="150"
|
||||||
onChange={(v) => setDirectHappyEyeballs({
|
onChange={onNumber((v) => setDirectHappyEyeballs({
|
||||||
...directHappyEyeballs,
|
...directHappyEyeballs,
|
||||||
tryDelayMs: typeof v === 'number' ? v : 0,
|
tryDelayMs: v,
|
||||||
})}
|
}))}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user