| 1 | import { assert, describe } from "@effect/vitest"; |
| 2 | |
| 3 | import { createOxlintRuleHarness } from "../test/utils.ts"; |
| 4 | |
| 5 | const rule = createOxlintRuleHarness("t3code/no-hermes-unsupported-apis", { |
| 6 | filename: "fixture.ts", |
| 7 | }); |
| 8 | |
| 9 | describe("t3code/no-hermes-unsupported-apis", () => { |
| 10 | rule.valid("allows in-place sort on a copy", `const sorted = [...items].sort(compare);`); |
| 11 | |
| 12 | rule.valid("allows in-place reverse on a copy", `const reversed = [...items].reverse();`); |
| 13 | |
| 14 | rule.valid( |
| 15 | "allows other ES2023 methods Hermes ships", |
| 16 | `const last = items.findLast(Boolean); const tail = items.at(-1);`, |
| 17 | ); |
| 18 | |
| 19 | rule.valid( |
| 20 | "ignores property reads that are not calls", |
| 21 | `const hasToSorted = typeof Array.prototype.toSorted === "function";`, |
| 22 | ); |
| 23 | |
| 24 | rule.invalid("reports toSorted", `const sorted = items.toSorted(compare);`, (output) => { |
| 25 | assert.match(output, /Array#toSorted/); |
| 26 | }); |
| 27 | |
| 28 | rule.invalid( |
| 29 | "reports toReversed in a chain", |
| 30 | `const open = chains.map((chain) => chain.layers.toReversed().filter(isOpen));`, |
| 31 | (output) => { |
| 32 | assert.match(output, /Array#toReversed/); |
| 33 | }, |
| 34 | ); |
| 35 | |
| 36 | rule.invalid( |
| 37 | "reports toSpliced via computed access with a copy-then-splice remediation", |
| 38 | `const next = items["toSpliced"](0, 1);`, |
| 39 | (output) => { |
| 40 | assert.match(output, /Array#toSpliced/); |
| 41 | assert.match(output, /const copy = \[\.\.\.array\]; copy\.splice\(\.\.\.\); use copy/); |
| 42 | }, |
| 43 | ); |
| 44 | |
| 45 | rule.invalid( |
| 46 | "reports a static template-literal property name", |
| 47 | "const reversed = items[`toReversed`]();", |
| 48 | (output) => { |
| 49 | assert.match(output, /Array#toReversed/); |
| 50 | }, |
| 51 | ); |
| 52 | |
| 53 | rule.valid( |
| 54 | "ignores a template-literal property with substitutions", |
| 55 | "const value = items[`to${suffix}`]();", |
| 56 | ); |
| 57 | |
| 58 | rule.valid("allows supported Intl constructors", `new Intl.NumberFormat();`); |
| 59 | rule.valid("allows feature detection", `const supported = typeof Intl.Segmenter === "function";`); |
| 60 | rule.valid("allows unrelated Segmenter constructors", `new custom.Segmenter();`); |
| 61 | rule.valid("ignores dynamic computed names", `new Intl[Segmenter](); items[toSorted]();`); |
| 62 | |
| 63 | for (const expression of [ |
| 64 | "new Intl.Segmenter(undefined, { granularity: 'grapheme' })", |
| 65 | "new Intl['Segmenter']()", |
| 66 | "new Intl[`Segmenter`]()", |
| 67 | "new globalThis.Intl.Segmenter()", |
| 68 | "new globalThis['Intl']['Segmenter']()", |
| 69 | "new global.Intl.Segmenter()", |
| 70 | "new window.Intl.Segmenter()", |
| 71 | "Intl.Segmenter()", |
| 72 | "Intl.Segmenter?.()", |
| 73 | ]) { |
| 74 | rule.invalid(`reports ${expression}`, `${expression};`, (output) => { |
| 75 | assert.match(output, /Hermes does not implement Intl\.Segmenter/); |
| 76 | assert.match(output, /portable implementation/); |
| 77 | }); |
| 78 | } |
| 79 | }); |