input-number-guard.mjs 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. // Ports the no-restricted-syntax selectors of the old ESLint config: oxlint has
  2. // no no-restricted-syntax, so the #6121/#6127 cleared-InputNumber guard lives here.
  3. const MESSAGE =
  4. 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).';
  5. const isLiteral = (node) =>
  6. !!node && /^(Literal|NumericLiteral|StringLiteral|BooleanLiteral)$/.test(node.type);
  7. function walk(node, visit) {
  8. if (!node || typeof node !== 'object') return;
  9. if (Array.isArray(node)) {
  10. for (const child of node) walk(child, visit);
  11. return;
  12. }
  13. if (typeof node.type === 'string') visit(node);
  14. for (const key of Object.keys(node)) {
  15. if (key !== 'parent') walk(node[key], visit);
  16. }
  17. }
  18. // `Number(v) || N`, `typeof v === 'number' ? v : N` and `v ?? N` all turn a
  19. // cleared field into a stored N.
  20. function isSyntheticClear(node) {
  21. if (node.type === 'LogicalExpression' && node.operator === '||') {
  22. return [node.left, node.right].some(
  23. (side) => side?.type === 'CallExpression' && side.callee?.name === 'Number',
  24. );
  25. }
  26. if (node.type === 'ConditionalExpression') {
  27. return node.test?.left?.operator === 'typeof' && isLiteral(node.alternate);
  28. }
  29. if (node.type === 'LogicalExpression' && node.operator === '??') {
  30. return isLiteral(node.right);
  31. }
  32. return false;
  33. }
  34. export default {
  35. meta: { name: 'input-number' },
  36. rules: {
  37. 'no-synthetic-clear': {
  38. create(context) {
  39. return {
  40. JSXElement(node) {
  41. if (node.openingElement?.name?.name !== 'InputNumber') return;
  42. for (const attr of node.openingElement.attributes ?? []) {
  43. if (attr.type !== 'JSXAttribute' || attr.name?.name !== 'onChange') continue;
  44. walk(attr.value, (inner) => {
  45. if (isSyntheticClear(inner)) context.report({ node: inner, message: MESSAGE });
  46. });
  47. }
  48. },
  49. };
  50. },
  51. },
  52. },
  53. };