From 7e136d688765a6ba2443470fb1945385686c8353 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 12 Sep 2026 08:17:33 -0300 Subject: [PATCH 1/2] refactor: satisfy the stricter lint rules and TypeScript flags Explicit return types and member accessibility everywhere, JSDoc param and return types on every exported function, no forEach, no parameter reassignment, no non-null assertions, no unsafe any, no default exports outside the config files, cyclomatic complexity under 20 and functions under 120 lines. The scripts parse the upstream payloads as unknown and validate their shape before writing a dataset. No public signature or output changes. --- scripts/banks.ts | 55 ++- scripts/cbo.ts | 21 +- scripts/cfop.ts | 16 +- scripts/cities.ts | 37 +- scripts/cnae.ts | 21 +- scripts/data.ts | 13 +- scripts/legal-natures.ts | 83 +++- scripts/llms.ts | 84 ++-- scripts/ncm.ts | 32 +- scripts/states.ts | 9 +- scripts/tree-shaking.ts | 151 +++++-- .../clamp-precision/clamp-precision.test.ts | 4 +- src/_internals/crc16-ccitt/crc16-ccitt.ts | 8 +- .../fetch-sorted-record.test.ts | 34 +- .../fetch-with-retry/fetch-with-retry.test.ts | 40 +- .../fetch-with-retry/fetch-with-retry.ts | 62 ++- src/_internals/format/format.ts | 13 +- .../generate-checksum/generate-checksum.ts | 4 +- src/_internals/is-nullish/is-nullish.test.ts | 4 +- .../is-repeated-digits/is-repeated-digits.ts | 7 +- .../is-valid-cei-cno-number.test.ts | 12 +- .../is-valid-pix-url/is-valid-pix-url.ts | 2 + src/_internals/mod11/mod11.test.ts | 2 +- src/_internals/mod11/mod11.ts | 1 - .../number-to-words/number-to-words.ts | 9 +- .../parse-arrecadacao.test.ts | 2 +- .../parse-decimal/parse-decimal.test.ts | 4 +- src/_internals/parse-decimal/parse-decimal.ts | 7 +- .../sanitize-to-alphanumeric.test.ts | 2 +- .../sanitize-to-alphanumeric.ts | 2 +- .../sanitize-to-ascii/sanitize-to-ascii.ts | 4 +- .../sanitize-to-digits.test.ts | 2 +- .../sanitize-to-digits/sanitize-to-digits.ts | 2 +- src/_internals/test/globals.d.ts | 18 +- src/_internals/test/runtime-bun.ts | 6 +- src/_internals/test/runtime-deno.ts | 419 ++++++++++-------- src/_internals/test/runtime.ts | 14 +- .../add-business-days.test.ts | 16 +- src/capitalize/capitalize.test.ts | 8 +- src/capitalize/capitalize.ts | 10 +- .../convert-currency-to-words.test.ts | 86 ++-- .../convert-date-to-words.test.ts | 38 +- .../convert-date-to-words.ts | 7 +- .../convert-license-plate-to-mercosul.test.ts | 6 +- .../convert-license-plate-to-mercosul.ts | 2 +- .../convert-number-to-words.test.ts | 98 ++-- .../difference-in-business-days.test.ts | 18 +- src/format-boleto/format-boleto.test.ts | 6 +- src/format-caepf/format-caepf.test.ts | 8 +- src/format-cei/format-cei.test.ts | 8 +- src/format-cep/format-cep.test.ts | 6 +- src/format-certidao/format-certidao.test.ts | 8 +- src/format-cnae/format-cnae.test.ts | 4 +- src/format-cnh/format-cnh.test.ts | 6 +- src/format-cno/format-cno.test.ts | 8 +- src/format-cnpj/format-cnpj.test.ts | 48 +- src/format-cnpj/format-cnpj.ts | 4 +- src/format-cns/format-cns.test.ts | 8 +- src/format-cpf/format-cpf.test.ts | 36 +- src/format-cpf/format-cpf.ts | 2 +- src/format-currency/format-currency.test.ts | 36 +- src/format-currency/format-currency.ts | 2 +- src/format-iban/format-iban.test.ts | 16 +- .../format-legal-nature.test.ts | 6 +- src/format-ncm/format-ncm.test.ts | 4 +- src/format-nfe-key/format-nfe-key.test.ts | 14 +- src/format-passport/format-passport.test.ts | 8 +- src/format-passport/format-passport.ts | 4 +- src/format-phone/format-phone.test.ts | 12 +- src/format-pis/format-pis.test.ts | 24 +- .../format-processo-juridico.test.ts | 6 +- src/format-voter-id/format-voter-id.test.ts | 6 +- src/generate-boleto/generate-boleto.ts | 4 +- src/generate-cnpj/generate-cnpj.ts | 11 +- src/generate-cpf/generate-cpf.test.ts | 2 +- .../generate-legal-nature.ts | 1 + .../generate-license-plate.test.ts | 2 +- .../generate-license-plate.ts | 13 +- src/generate-passport/generate-passport.ts | 2 +- src/generate-pis/generate-pis.ts | 7 +- .../generate-pix-payload.test.ts | 36 +- .../generate-pix-payload.ts | 92 ++-- .../generate-processo-juridico.test.ts | 6 +- .../generate-voter-id.test.ts | 4 +- .../get-address-info-by-cep.test.ts | 74 +++- .../get-address-info-by-cep.ts | 120 ++--- .../get-area-code-info.test.ts | 6 +- src/get-area-code-info/get-area-code-info.ts | 4 +- .../get-area-codes-by-state.test.ts | 8 +- src/get-bank-by-code/get-bank-by-code.test.ts | 14 +- src/get-bank-by-ispb/get-bank-by-ispb.test.ts | 14 +- src/get-banks/get-banks.test.ts | 14 +- src/get-banks/get-banks.ts | 2 +- src/get-boleto-info/get-boleto-info.test.ts | 4 +- src/get-cbo/get-cbo.test.ts | 6 +- src/get-cbo/get-cbo.ts | 6 +- .../get-cep-info-by-address.test.ts | 12 +- .../get-cep-info-by-address.ts | 14 +- src/get-cfop/get-cfop.test.ts | 2 +- src/get-cfop/get-cfop.ts | 6 +- src/get-cities/get-cities.test.ts | 8 +- src/get-cnae/get-cnae.test.ts | 6 +- src/get-cnae/get-cnae.ts | 6 +- src/get-holidays/get-holidays.test.ts | 85 ++-- src/get-holidays/get-holidays.ts | 50 +-- src/get-legal-nature/get-legal-nature.test.ts | 2 +- src/get-legal-natures/get-legal-natures.ts | 13 +- .../get-municipalities.test.ts | 13 +- .../get-municipality-by-code.test.ts | 16 +- src/get-municipality/get-municipality.test.ts | 24 +- src/get-municipality/get-municipality.ts | 8 +- .../get-state-by-ibge-code.test.ts | 6 +- .../get-state-code-by-name.test.ts | 8 +- .../get-state-name-by-code.test.ts | 8 +- src/get-states/get-states.test.ts | 15 +- src/get-states/get-states.ts | 2 +- .../get-timezone-by-state.test.ts | 8 +- src/index.test.ts | 4 +- src/is-business-day/is-business-day.test.ts | 10 +- src/is-business-day/is-business-day.ts | 4 +- src/is-holiday/is-holiday.test.ts | 10 +- src/is-holiday/is-holiday.ts | 3 +- .../is-valid-bank-account.test.ts | 26 +- .../is-valid-bank-account.ts | 9 +- src/is-valid-boleto/is-valid-boleto.test.ts | 14 +- src/is-valid-boleto/is-valid-boleto.ts | 6 +- src/is-valid-caepf/is-valid-caepf.test.ts | 10 +- src/is-valid-cbo/is-valid-cbo.test.ts | 4 +- src/is-valid-cei/is-valid-cei.test.ts | 10 +- src/is-valid-cep/is-valid-cep.test.ts | 10 +- .../is-valid-certidao.test.ts | 10 +- src/is-valid-cfop/is-valid-cfop.test.ts | 2 +- src/is-valid-cnae/is-valid-cnae.test.ts | 4 +- src/is-valid-cnh/is-valid-cnh.test.ts | 6 +- src/is-valid-cno/is-valid-cno.test.ts | 10 +- src/is-valid-cnpj/is-valid-cnpj.test.ts | 14 +- src/is-valid-cnpj/is-valid-cnpj.ts | 16 +- src/is-valid-cns/is-valid-cns.test.ts | 14 +- src/is-valid-cpf/is-valid-cpf.test.ts | 14 +- .../is-valid-credit-card.test.ts | 16 +- src/is-valid-csosn/is-valid-csosn.test.ts | 2 +- src/is-valid-cst/is-valid-cst.test.ts | 7 +- src/is-valid-cst/is-valid-cst.ts | 22 +- src/is-valid-email/is-valid-email.test.ts | 6 +- src/is-valid-iban/is-valid-iban.test.ts | 18 +- src/is-valid-ie/is-valid-ie.test.ts | 20 +- src/is-valid-ie/is-valid-ie.ts | 48 +- .../is-valid-landline-phone.test.ts | 10 +- .../is-valid-legal-nature.test.ts | 6 + .../is-valid-legal-nature.ts | 2 +- .../is-valid-license-plate.test.ts | 14 +- .../is-valid-mobile-phone.test.ts | 10 +- .../is-valid-mobile-phone.ts | 2 +- src/is-valid-ncm/is-valid-ncm.test.ts | 6 +- src/is-valid-nfe-key/is-valid-nfe-key.test.ts | 16 +- .../is-valid-passport.test.ts | 8 +- src/is-valid-passport/is-valid-passport.ts | 4 +- src/is-valid-phone/is-valid-phone.test.ts | 10 +- src/is-valid-pis/is-valid-pis.test.ts | 16 +- src/is-valid-pis/is-valid-pis.ts | 2 +- src/is-valid-pix-key/is-valid-pix-key.test.ts | 20 +- .../is-valid-pix-payload.test.ts | 16 +- .../is-valid-processo-juridico.test.ts | 6 +- .../is-valid-processo-juridico.ts | 6 +- .../is-valid-registro-profissional.test.ts | 6 +- .../is-valid-registro-profissional.ts | 6 +- src/is-valid-renavam/is-valid-renavam.test.ts | 16 +- src/is-valid-renavam/is-valid-renavam.ts | 16 +- .../is-valid-service-phone.test.ts | 10 +- src/is-valid-vin/is-valid-vin.test.ts | 22 +- src/is-valid-vin/is-valid-vin.ts | 4 +- .../is-valid-voter-id.test.ts | 14 +- src/parse-boleto/parse-boleto.test.ts | 6 +- src/parse-certidao/parse-certidao.test.ts | 6 +- src/parse-certidao/parse-certidao.ts | 6 +- src/parse-currency/parse-currency.test.ts | 22 +- src/parse-iban/parse-iban.test.ts | 10 +- src/parse-nfe-key/parse-nfe-key.test.ts | 8 +- src/parse-nfe-key/parse-nfe-key.ts | 6 +- src/parse-passport/parse-passport.ts | 4 +- src/parse-phone/parse-phone.test.ts | 8 +- src/parse-pix-key/parse-pix-key.test.ts | 16 +- .../parse-pix-payload.test.ts | 64 ++- src/parse-pix-payload/parse-pix-payload.ts | 153 ++++--- src/parse-voter-id/parse-voter-id.test.ts | 6 +- src/remove-accents/remove-accents.test.ts | 8 +- 186 files changed, 1999 insertions(+), 1470 deletions(-) diff --git a/scripts/banks.ts b/scripts/banks.ts index 0d9ccf7b..c5b3cbe5 100644 --- a/scripts/banks.ts +++ b/scripts/banks.ts @@ -1,12 +1,11 @@ #!/usr/bin/env node import { readFile, writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; const BACEN_CSV_URL = "https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv"; @@ -26,6 +25,14 @@ type BrasilApiBank = { fullName?: string; }; +const isBrasilApiBank = (value: unknown): value is BrasilApiBank => + typeof value === "object" && + value !== null && + (!("ispb" in value) || typeof value.ispb === "string") && + (!("code" in value) || typeof value.code === "number") && + (!("name" in value) || typeof value.name === "string") && + (!("fullName" in value) || typeof value.fullName === "string"); + const parseCsvLine = (line: string): string[] => { const fields: string[] = []; let current = ""; @@ -73,7 +80,17 @@ const fetchFromBacen = async (): Promise => { for (const row of rows) { const [ispb, , code, , , name] = parseCsvLine(row); - if (!ispb || !code || !name || !/^\d{1,3}$/.test(code)) continue; + if ( + ispb === undefined || + ispb === "" || + code === undefined || + code === "" || + name === undefined || + name === "" || + !/^\d{1,3}$/.test(code) + ) { + continue; + } banks.push({ code: code.padStart(3, "0"), ispb, name: name.trim() }); } @@ -88,31 +105,33 @@ const fetchFromBrasilApi = async (): Promise => { throw new Error(`BrasilAPI banks request failed with status ${response.status}`); } - const json: BrasilApiBank[] = await response.json(); + const json: unknown = await response.json(); + + if (!Array.isArray(json)) { + throw new TypeError("BrasilAPI banks payload is not an array"); + } const banks: BankRow[] = []; - for (const bank of json) { - if ( - typeof bank.code !== "number" || - !Number.isInteger(bank.code) || - bank.code < 0 || - bank.code > 999 || - !bank.ispb - ) - continue; + for (const entry of json) { + if (!isBrasilApiBank(entry) || typeof entry.code !== "number") continue; + if (!Number.isInteger(entry.code) || entry.code < 0 || entry.code > 999) continue; + + const ispb = entry.ispb; + + if (ispb === undefined || ispb === "") continue; - const name = (bank.fullName ?? bank.name ?? "").trim(); + const name = (entry.fullName ?? entry.name ?? "").trim(); - if (!name) continue; + if (name === "") continue; - banks.push({ code: String(bank.code).padStart(3, "0"), ispb: bank.ispb, name }); + banks.push({ code: String(entry.code).padStart(3, "0"), ispb, name }); } return banks; }; -const main = async () => { +const main = async (): Promise => { let banks: BankRow[]; let source: string; diff --git a/scripts/cbo.ts b/scripts/cbo.ts index f62ef574..e00b48fc 100644 --- a/scripts/cbo.ts +++ b/scripts/cbo.ts @@ -1,24 +1,35 @@ #!/usr/bin/env node import { writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { fetchSortedRecord } from "../src/_internals/fetch-sorted-record/fetch-sorted-record.ts"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; type CboEntry = { cbo: string; descricao: string; }; -const main = async () => { +const isCboEntry = (value: unknown): value is CboEntry => + typeof value === "object" && + value !== null && + "cbo" in value && + typeof value.cbo === "string" && + "descricao" in value && + typeof value.descricao === "string"; + +const main = async (): Promise => { const sorted = await fetchSortedRecord( "https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json", "CBO mirror", async (response) => { - const json: CboEntry[] = await response.json(); + const json: unknown = await response.json(); + + if (!Array.isArray(json) || !json.every((entry) => isCboEntry(entry))) { + throw new Error("CBO mirror payload is not an array of cbo and descricao entries"); + } const data: Record = {}; diff --git a/scripts/cfop.ts b/scripts/cfop.ts index 22e9ad2f..cc3c2a22 100644 --- a/scripts/cfop.ts +++ b/scripts/cfop.ts @@ -1,12 +1,11 @@ #!/usr/bin/env node import { writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { fetchSortedRecord } from "../src/_internals/fetch-sorted-record/fetch-sorted-record.ts"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; const EMBEDDED_ENTRY_REGEX = /\s+(\d)\.(\d{3})\s+-\s+/g; @@ -14,6 +13,9 @@ const EMBEDDED_ENTRY_REGEX = /\s+(\d)\.(\d{3})\s+-\s+/g; * Some rows of the mirror glue the next code into the description, e.g. * `1305;"... energia elétrica 1.306 - Aquisição de serviço ..."`, which both corrupts the * `1305` description and drops `1306`. Splits such a row into one entry per code. + * @param {string} code - The CFOP code the row started with. + * @param {string} description - The row description, possibly containing embedded codes. + * @returns {[string, string][]} One `[code, description]` entry per code found in the row. */ const splitEmbeddedEntries = (code: string, description: string): [string, string][] => { const entries: [string, string][] = []; @@ -23,18 +25,18 @@ const splitEmbeddedEntries = (code: string, description: string): [string, strin for (const match of description.matchAll(EMBEDDED_ENTRY_REGEX)) { entries.push([ currentCode, - description.slice(lastIndex, match.index).replace(/\s+/g, " ").trim(), + description.slice(lastIndex, match.index).replaceAll(/\s+/g, " ").trim(), ]); currentCode = `${match[1]}${match[2]}`; lastIndex = match.index + match[0].length; } - entries.push([currentCode, description.slice(lastIndex).replace(/\s+/g, " ").trim()]); + entries.push([currentCode, description.slice(lastIndex).replaceAll(/\s+/g, " ").trim()]); return entries; }; -const main = async () => { +const main = async (): Promise => { const sorted = await fetchSortedRecord( "https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv", "CFOP mirror", @@ -50,6 +52,8 @@ const main = async () => { const [, code, description] = match; + if (code === undefined || description === undefined) continue; + for (const [entryCode, entryDescription] of splitEmbeddedEntries(code, description)) { if (entryCode.endsWith("00")) continue; diff --git a/scripts/cities.ts b/scripts/cities.ts index a2384dff..11a32d7d 100644 --- a/scripts/cities.ts +++ b/scripts/cities.ts @@ -1,13 +1,12 @@ #!/usr/bin/env node import { writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { DATA as STATES } from "../src/_internals/constants/states.ts"; import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; const STATE_CODES = STATES.map((state) => state.code); @@ -47,7 +46,17 @@ type City = { }; }; -const main = async () => { +const isCity = (value: unknown): value is City => + typeof value === "object" && + value !== null && + "id" in value && + typeof value.id === "number" && + "nome" in value && + typeof value.nome === "string" && + "microrregiao" in value && + "regiao-imediata" in value; + +const main = async (): Promise => { const response = await fetchWithRetry( "https://servicodados.ibge.gov.br/api/v1/localidades/municipios", ); @@ -56,21 +65,23 @@ const main = async () => { throw new Error(`IBGE municipalities request failed with status ${response.status}`); } - const json: City[] = await response.json(); + const json: unknown = await response.json(); + + if (!Array.isArray(json) || !json.every((entry) => isCity(entry))) { + throw new Error("IBGE municipalities payload is not an array of city entries"); + } const byState = json.reduce( (acc, city) => { const stateInitials = - city?.microrregiao?.mesorregiao?.UF?.sigla ?? - city?.["regiao-imediata"]?.["regiao-intermediaria"]?.UF?.sigla; + city.microrregiao?.mesorregiao?.UF?.sigla ?? + city["regiao-imediata"]?.["regiao-intermediaria"]?.UF?.sigla; - if (!stateInitials) return acc; + if (stateInitials === undefined || stateInitials === "") return acc; - if (!acc[stateInitials]) { - acc[stateInitials] = []; - } + const cityNames = (acc[stateInitials] ??= []); - acc[stateInitials].push([city.nome, String(city.id)]); + cityNames.push([city.nome, String(city.id)]); return acc; }, @@ -91,7 +102,7 @@ const main = async () => { }) .join("\n"); - const missingStates = STATE_CODES.filter((code) => !byState[code]); + const missingStates = STATE_CODES.filter((code) => !Object.hasOwn(byState, code)); if (missingStates.length > 0) { throw new Error(`IBGE response is missing municipalities for: ${missingStates.join(", ")}`); diff --git a/scripts/cnae.ts b/scripts/cnae.ts index 4d63850f..2a889ee7 100644 --- a/scripts/cnae.ts +++ b/scripts/cnae.ts @@ -1,26 +1,37 @@ #!/usr/bin/env node import { writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; type CnaeSubclass = { id: string; descricao: string; }; -const main = async () => { +const isCnaeSubclass = (value: unknown): value is CnaeSubclass => + typeof value === "object" && + value !== null && + "id" in value && + typeof value.id === "string" && + "descricao" in value && + typeof value.descricao === "string"; + +const main = async (): Promise => { const response = await fetchWithRetry("https://servicodados.ibge.gov.br/api/v2/cnae/subclasses"); if (!response.ok) { throw new Error(`IBGE CNAE request failed with status ${response.status}`); } - const json: CnaeSubclass[] = await response.json(); + const json: unknown = await response.json(); + + if (!Array.isArray(json) || !json.every((entry) => isCnaeSubclass(entry))) { + throw new Error("IBGE CNAE payload is not an array of subclass entries"); + } const entries = json .filter((subclass) => /^\d{7}$/.test(subclass.id)) diff --git a/scripts/data.ts b/scripts/data.ts index c6e8ea9a..1066217f 100644 --- a/scripts/data.ts +++ b/scripts/data.ts @@ -1,10 +1,9 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; const run = (command: string, args: string[]): Promise => new Promise((resolveExit) => { @@ -12,8 +11,12 @@ const run = (command: string, args: string[]): Promise => stdio: "inherit", }); - child.on("close", (code) => resolveExit(code)); - child.on("error", () => resolveExit(1)); + child.on("close", (code) => { + resolveExit(code); + }); + child.on("error", () => { + resolveExit(1); + }); }); const generators = [ diff --git a/scripts/legal-natures.ts b/scripts/legal-natures.ts index bb65444a..c06f337c 100644 --- a/scripts/legal-natures.ts +++ b/scripts/legal-natures.ts @@ -1,13 +1,12 @@ #!/usr/bin/env node import { writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { inflateSync } from "node:zlib"; import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; const SOURCE_URL = "https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf"; @@ -51,7 +50,9 @@ const inflateStreams = (pdf: Buffer): string[] => { try { streams.push(inflateSync(pdf.subarray(contentStart, end)).toString("latin1")); - } catch {} + } catch (error) { + if (!(error instanceof Error)) throw error; + } cursor = end + "endstream".length; } @@ -60,38 +61,72 @@ const inflateStreams = (pdf: Buffer): string[] => { }; const unescapePdfString = (value: string): string => - value.replace(/\\([0-7]{1,3})|\\(.)/g, (_match, octal?: string, char?: string) => { - if (octal) return String.fromCharCode(Number.parseInt(octal, 8)); + value.replaceAll(/\\([0-7]{1,3})|\\(.)/g, (_match, octal?: string, char?: string) => { + if (octal !== undefined) return String.fromCharCode(Number.parseInt(octal, 8)); if (char === "n") return "\n"; if (char === "r") return "\r"; if (char === "t") return "\t"; return char ?? ""; }); +const extractBracketText = (body: string): string => { + let text = ""; + + for (const array of body.matchAll(/\[((?:[^[\]\\]|\\.)*)\]\s*TJ/g)) { + const arrayContent = array[1]; + + if (arrayContent === undefined) continue; + + for (const chunk of arrayContent.matchAll(/\(((?:[^()\\]|\\.)*)\)/g)) { + const chunkText = chunk[1]; + + if (chunkText === undefined) continue; + + text += unescapePdfString(chunkText); + } + } + + return text; +}; + +const extractParenthesizedText = (body: string): string => { + let text = ""; + + for (const chunk of body.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { + const chunkText = chunk[1]; + + if (chunkText === undefined) continue; + + text += unescapePdfString(chunkText); + } + + return text; +}; + const extractLines = (streams: string[]): string[] => { const lines: string[] = []; for (const stream of streams) { - const rows = new Map>(); + const rows = new Map(); for (const block of stream.matchAll(/BT([\s\S]*?)ET/g)) { const body = block[1]; + + if (body === undefined) continue; + const matrix = [...body.matchAll(/([-\d.]+)\s+([-\d.]+)\s+Tm/g)].pop(); if (!matrix) continue; - let text = ""; - for (const array of body.matchAll(/\[((?:[^[\]\\]|\\.)*)\]\s*TJ/g)) { - for (const chunk of array[1].matchAll(/\(((?:[^()\\]|\\.)*)\)/g)) { - text += unescapePdfString(chunk[1]); - } - } - for (const chunk of body.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { - text += unescapePdfString(chunk[1]); - } + const [, rawX, rawY] = matrix; + + if (rawX === undefined || rawY === undefined) continue; + + const text = extractBracketText(body) + extractParenthesizedText(body); + if (!text) continue; - const x = Number.parseFloat(matrix[1]); - const y = Math.round(Number.parseFloat(matrix[2]) * 10) / 10; + const x = Number.parseFloat(rawX); + const y = Math.round(Number.parseFloat(rawY) * 10) / 10; const row = rows.get(y) ?? []; row.push([x, text]); @@ -119,8 +154,14 @@ const parseLegalNatures = (lines: string[]): Record => { const match = line.match(/^\s*(\d{3})-(\d)\s*-\s*(.+?)\s*$/); if (!match) continue; - const code = `${match[1]}${match[2]}`; - const description = match[3].replace(/\s+/g, " ").trim(); + const [, codePrefix, codeSuffix, rawDescription] = match; + + if (codePrefix === undefined || codeSuffix === undefined || rawDescription === undefined) { + continue; + } + + const code = `${codePrefix}${codeSuffix}`; + const description = rawDescription.replaceAll(/\s+/g, " ").trim(); legalNatures[code] = TYPO_FIXES[description] ?? description; } @@ -133,7 +174,7 @@ const stringifyEntries = (entries: Record): string => .map(([code, description]) => `\t${JSON.stringify(code)}: ${JSON.stringify(description)},`) .join("\n"); -const main = async () => { +const main = async (): Promise => { const response = await fetchWithRetry(SOURCE_URL); if (!response.ok) { diff --git a/scripts/llms.ts b/scripts/llms.ts index 4216275c..bf401b2a 100644 --- a/scripts/llms.ts +++ b/scripts/llms.ts @@ -6,11 +6,11 @@ const DOCS_DIR = join(ROOT, "docs"); const SITE = "https://brazilian-utils.com.br"; const REPO = "https://github.com/brazilian-utils/javascript"; -interface UtilSection { +type UtilSection = { name: string; slug: string; description: string; -} +}; const SLUG_STRIP_PATTERN = new RegExp( "[\\u2000-\\u206F\\u2E00-\\u2E7F\\\\'!\"#$%&()*+,./:;<=>?@\\[\\]^`{|}~]", @@ -19,43 +19,53 @@ const SLUG_STRIP_PATTERN = new RegExp( const VARIATION_SELECTOR_PATTERN = new RegExp("\\uFE0F", "g"); const EMOJI_PATTERN = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu; +/** + * Removes every match of `pattern` repeatedly until nothing changes, so nested or overlapping + * matches cannot survive a single pass. + * @param {string} value - The string to strip matches from. + * @param {RegExp} pattern - The pattern to remove, repeatedly. + * @returns {string} `value` with every match of `pattern` removed. + */ +function removeUntilStable(value: string, pattern: RegExp): string { + let current = value; + let previous = ""; + while (current !== previous) { + previous = current; + current = current.replace(pattern, ""); + } + return current; +} + /** * Reproduces docsify's heading-to-anchor slug algorithm (see * `src/core/render/slugify.js` in the docsify source) so links into * `utilities.md`/`getting-started.md` resolve to the same anchors docsify * renders at runtime. + * @param {string} heading - The Markdown heading text to slugify. + * @returns {string} The docsify-compatible anchor slug for `heading`. */ function slugify(heading: string): string { return removeUntilStable(heading.trim().normalize("NFC"), /<[^>]+>/g) - .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replaceAll(/\[([^\]]+)\]\([^)]+\)/g, "$1") .replace(VARIATION_SELECTOR_PATTERN, "") .replace(EMOJI_PATTERN, "") - .replace(/[A-Z]+/g, (match) => match.toLowerCase()) + .replaceAll(/[A-Z]+/g, (match) => match.toLowerCase()) .replace(SLUG_STRIP_PATTERN, "") - .replace(/\s/g, "-") + .replaceAll(/\s/g, "-") .replace(/^(\d)/, "_$1"); } -/** Removes every match of `pattern` repeatedly until nothing changes, so nested or overlapping matches cannot survive a single pass. */ -function removeUntilStable(value: string, pattern: RegExp): string { - let current = value; - let previous = ""; - while (current !== previous) { - previous = current; - current = current.replace(pattern, ""); - } - return current; -} - const ABBREVIATION_PLACEHOLDER = String.fromCharCode(1); /** * Extracts the first sentence of a paragraph, treating `e.g.`/`i.e.` as * abbreviations rather than sentence boundaries. + * @param {string} paragraph - The paragraph to extract the first sentence from. + * @returns {string} The first sentence of `paragraph`. */ function firstSentence(paragraph: string): string { - const withoutLinks = paragraph.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); - const protectedText = withoutLinks.replace( + const withoutLinks = paragraph.replaceAll(/\[([^\]]+)\]\([^)]+\)/g, "$1"); + const protectedText = withoutLinks.replaceAll( /\b(e\.g|i\.e)\./gi, (_match, abbr: string) => `${abbr}${ABBREVIATION_PLACEHOLDER}`, ); @@ -65,7 +75,11 @@ function firstSentence(paragraph: string): string { return sentence.split(ABBREVIATION_PLACEHOLDER).join(".").trim(); } -/** Parses every `## ` section of `utilities.md` into name/slug/description. */ +/** + * Parses every `## ` section of `utilities.md` into name/slug/description. + * @param {string} utilitiesMd - The full contents of `utilities.md`. + * @returns {UtilSection[]} One entry per `## ` section, in document order. + */ function parseUtilities(utilitiesMd: string): UtilSection[] { const sections = utilitiesMd.split(/^## /m).slice(1); @@ -73,7 +87,8 @@ function parseUtilities(utilitiesMd: string): UtilSection[] { const newlineIndex = section.indexOf("\n"); const name = section.slice(0, newlineIndex).trim(); const body = section.slice(newlineIndex + 1); - const firstParagraph = body.split(/\n\s*\n/)[0].trim(); + const [firstParagraphRaw = ""] = body.split(/\n\s*\n/); + const firstParagraph = firstParagraphRaw.trim(); return { name, @@ -91,7 +106,7 @@ const PREFIX_GROUPS: { title: string; test: (name: string) => boolean }[] = [ { title: "Getters (get*)", test: (name) => name.startsWith("get") }, ]; -function groupUtilities(utils: UtilSection[]) { +function groupUtilities(utils: UtilSection[]): { title: string; utils: UtilSection[] }[] { const groups: { title: string; utils: UtilSection[] }[] = PREFIX_GROUPS.map((group) => ({ title: group.title, utils: [], @@ -100,11 +115,12 @@ function groupUtilities(utils: UtilSection[]) { for (const util of utils) { const groupIndex = PREFIX_GROUPS.findIndex((group) => group.test(util.name)); + const matchedGroup = groupIndex === -1 ? undefined : groups[groupIndex]; - if (groupIndex === -1) { + if (matchedGroup === undefined) { other.push(util); } else { - groups[groupIndex].utils.push(util); + matchedGroup.utils.push(util); } } @@ -161,16 +177,30 @@ ${groupSections} `; } -/** Strips docsify-only markdown syntax (`?id=` anchors, HTML comments) so the content reads as plain Markdown. */ +/** + * Strips docsify-only markdown syntax (`?id=` anchors, HTML comments) so the content reads as + * plain Markdown. + * @param {string} markdown - The docsify-flavored Markdown to strip. + * @returns {string} `markdown` with docsify-only syntax removed. + */ function stripDocsifySyntax(markdown: string): string { return removeUntilStable(markdown, //g) - .replace(/\]\(([^)]+)\?id=([^)]+)\)/g, (_match, path: string, id: string) => `](${path}#${id})`) + .replaceAll( + /\]\(([^)]+)\?id=([^)]+)\)/g, + (_match, path: string, id: string) => `](${path}#${id})`, + ) .trimEnd(); } -/** Demotes every markdown heading in `markdown` by `levels` (adds `#`s), so it nests under a higher-level heading. */ +/** + * Demotes every markdown heading in `markdown` by `levels` (adds `#`s), so it nests under a + * higher-level heading. + * @param {string} markdown - The Markdown whose headings should be demoted. + * @param {number} levels - How many `#`s to add to each heading. + * @returns {string} `markdown` with every heading demoted by `levels`. + */ function demoteHeadings(markdown: string, levels: number): string { - return markdown.replace( + return markdown.replaceAll( /^(#{1,5})(\s)/gm, (_match, hashes: string, space: string) => `${"#".repeat(hashes.length + levels)}${space}`, ); diff --git a/scripts/ncm.ts b/scripts/ncm.ts index b74db86e..d18471d3 100644 --- a/scripts/ncm.ts +++ b/scripts/ncm.ts @@ -1,12 +1,11 @@ #!/usr/bin/env node import { writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; type NcmEntry = { Codigo: string; @@ -17,7 +16,22 @@ type NcmResponse = { Nomenclaturas: NcmEntry[]; }; -const main = async () => { +const isNcmEntry = (value: unknown): value is NcmEntry => + typeof value === "object" && + value !== null && + "Codigo" in value && + typeof value.Codigo === "string" && + "Data_Fim" in value && + typeof value.Data_Fim === "string"; + +const isNcmResponse = (value: unknown): value is NcmResponse => + typeof value === "object" && + value !== null && + "Nomenclaturas" in value && + Array.isArray(value.Nomenclaturas) && + value.Nomenclaturas.every((entry) => isNcmEntry(entry)); + +const main = async (): Promise => { const response = await fetchWithRetry( "https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json?perfil=PUBLICO", ); @@ -26,15 +40,19 @@ const main = async () => { throw new Error(`Siscomex NCM request failed with status ${response.status}`); } - const json: NcmResponse = await response.json(); + const json: unknown = await response.json(); + + if (!isNcmResponse(json)) { + throw new Error("Siscomex NCM payload is not a Nomenclaturas response"); + } const codes = json.Nomenclaturas.filter( (entry) => entry.Data_Fim === "31/12/9999" && /^[\d.]{10}$/.test(entry.Codigo), ) - .map((entry) => entry.Codigo.replace(/\D/g, "")) + .map((entry) => entry.Codigo.replaceAll(/\D/g, "")) .filter((code) => code.length === 8); - const uniqueSortedCodes = Array.from(new Set(codes)).sort(); + const uniqueSortedCodes = [...new Set(codes)].sort(); await writeFile( resolve(scriptsDir, "..", "./src/is-valid-ncm/constants.ts"), diff --git a/scripts/states.ts b/scripts/states.ts index 9468e43e..814d97f7 100644 --- a/scripts/states.ts +++ b/scripts/states.ts @@ -1,12 +1,11 @@ #!/usr/bin/env node import { writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; -const scriptsDir = dirname(fileURLToPath(import.meta.url)); +const scriptsDir = import.meta.dirname; type State = { id: number; @@ -36,7 +35,7 @@ const isState = (value: unknown): value is State => "nome" in value.regiao && typeof value.regiao.nome === "string"; -const main = async () => { +const main = async (): Promise => { const response = await fetchWithRetry( "https://servicodados.ibge.gov.br/api/v1/localidades/estados", ); @@ -47,7 +46,7 @@ const main = async () => { const json: unknown = await response.json(); - if (!Array.isArray(json) || json.length === 0 || !json.every(isState)) { + if (!Array.isArray(json) || json.length === 0 || !json.every((entry) => isState(entry))) { throw new Error( "IBGE states payload is not an array of states with id, sigla, nome and regiao", ); diff --git a/scripts/tree-shaking.ts b/scripts/tree-shaking.ts index 3a50c822..9bd66de4 100644 --- a/scripts/tree-shaking.ts +++ b/scripts/tree-shaking.ts @@ -31,13 +31,13 @@ import { existsSync } from "node:fs"; import { mkdir, mkdtemp, readFile, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; import { gzipSync } from "node:zlib"; import { build } from "esbuild"; -const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const rootDir = resolve(import.meta.dirname, ".."); const packageName = "@brazilian-utils/brazilian-utils"; const CONCURRENCY = 16; @@ -102,12 +102,21 @@ const mapWithConcurrency = async ( const results: R[] = Array.from({ length: items.length }); let cursor = 0; - const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { - while (cursor < items.length) { - const index = cursor++; - results[index] = await fn(items[index]); + const worker = async (): Promise => { + const index = cursor++; + + if (index >= items.length) return; + + const item = items[index]; + + if (item !== undefined) { + results[index] = await fn(item); } - }); + + await worker(); + }; + + const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker()); await Promise.all(workers); return results; @@ -131,7 +140,13 @@ const bundleSource = async ( write: false, logLevel: "error", }); - return result.outputFiles[0].contents; + const outputFile = result.outputFiles[0]; + + if (outputFile === undefined) { + throw new Error(`esbuild produced no output for ${sourcefile}`); + } + + return outputFile.contents; }; const measure = async ( @@ -147,10 +162,18 @@ const measure = async ( }; }; +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + const loadExports = async ( distEntry: string, ): Promise<{ testable: string[]; aliasOf: Map }> => { - const mod: Record = await import(pathToFileURL(distEntry).href); + const mod: unknown = await import(pathToFileURL(distEntry).href); + + if (!isRecord(mod)) { + throw new Error(`Unexpected default export shape for ${distEntry}`); + } + const functionExports = Object.keys(mod) .filter((name) => typeof mod[name] === "function") .sort(); @@ -167,7 +190,10 @@ const loadExports = async ( for (const group of groups.values()) { if (group.length < 2) continue; const sorted = [...group].sort(); - const target = sorted[sorted.length - 1]; + const target = sorted.at(-1); + + if (target === undefined) continue; + for (const name of sorted.slice(0, -1)) aliasOf.set(name, target); } @@ -213,17 +239,27 @@ const measureExports = async ( ({ name, names }) => measure(name, importSource(names), resolveDir), ); + if (full === undefined) { + throw new Error("No measurements produced"); + } + const exportsMap: Record = {}; for (const m of measurements) exportsMap[m.name] = { bytes: m.bytes, gzip: m.gzip }; for (const [alias, target] of aliasOf) { - const targetMeasurement = exportsMap[target]; - if (targetMeasurement) exportsMap[alias] = targetMeasurement; + const targetMeasurement: Measurement | undefined = exportsMap[target]; + if (targetMeasurement !== undefined) exportsMap[alias] = targetMeasurement; } return { full: { bytes: full.bytes, gzip: full.gzip }, exports: exportsMap, aliasOf, resolveDir }; }; -/** Bundles one named import of every name in `names` at once, the way a consumer using that whole API surface would. */ +/** + * Bundles one named import of every name in `names` at once, the way a consumer using that whole + * API surface would. + * @param {string[]} names - The export names to bundle together. + * @param {string} resolveDir - The directory esbuild resolves the bundled import from. + * @returns {Promise} The bundled size, in bytes and gzip bytes. + */ const measureNames = async (names: string[], resolveDir: string): Promise => { if (names.length === 0) return { bytes: 0, gzip: 0 }; const { bytes, gzip } = await measure("__existing__", importSource(names), resolveDir); @@ -254,13 +290,14 @@ const compareSnapshots = (base: Snapshot, head: Snapshot, existing: Measurement) const removed: (Measurement & { name: string })[] = []; for (const name of [...names].sort()) { - const baseMeasurement = base.exports[name]; - const headMeasurement = head.exports[name]; - if (!baseMeasurement) { + const baseMeasurement: Measurement | undefined = base.exports[name]; + const headMeasurement: Measurement | undefined = head.exports[name]; + if (baseMeasurement === undefined) { + if (headMeasurement === undefined) continue; added.push({ name, ...headMeasurement }); continue; } - if (!headMeasurement) { + if (headMeasurement === undefined) { removed.push({ name, ...baseMeasurement }); continue; } @@ -310,25 +347,24 @@ const renderMarkdown = ( existing: Measurement, result: CompareResult, ): string => { - const lines: string[] = []; - lines.push("## Tree-shaking report"); - lines.push(""); - lines.push( + const lines: string[] = [ + "## Tree-shaking report", + "", `Fails when a pre-existing export grows more than ${REGRESSION_PERCENT_THRESHOLD * 100}% and more than ` + `${REGRESSION_BYTES_THRESHOLD} B, or when importing every export that already existed on the base ` + `grows more than ${FULL_IMPORT_PERCENT_THRESHOLD * 100}%. New exports never count as a regression.`, - ); - lines.push(""); - lines.push( + "", `Pre-existing exports: ${base.full.bytes} B to ${existing.bytes} B (${formatPercent(result.fullDeltaPercent)}, ` + `gzip ${existing.gzip} B)${result.fullImportRegressed ? ", REGRESSION" : ""}. ` + `Full import on head: ${head.full.bytes} B (gzip ${head.full.gzip} B).`, - ); - lines.push(""); + "", + ]; if (result.changed.length > 0) { - lines.push("| name | base | head | delta bytes | delta % | gzip head |"); - lines.push("| --- | --- | --- | --- | --- | --- |"); + lines.push( + "| name | base | head | delta bytes | delta % | gzip head |", + "| --- | --- | --- | --- | --- | --- |", + ); for (const row of result.changed) { const flag = result.regressions.includes(row) ? " (REGRESSION)" : ""; lines.push( @@ -340,47 +376,72 @@ const renderMarkdown = ( } if (result.added.length > 0) { - lines.push("**New exports**"); - lines.push(""); + lines.push("**New exports**", ""); for (const item of result.added) lines.push(`- ${item.name}: ${item.bytes} B (gzip ${item.gzip} B)`); lines.push(""); } if (result.removed.length > 0) { - lines.push("**Removed exports**"); - lines.push(""); + lines.push("**Removed exports**", ""); for (const item of result.removed) lines.push(`- ${item.name}: was ${item.bytes} B`); lines.push(""); } if (result.unchanged.length > 0) { - lines.push(`
Unchanged exports (${result.unchanged.length})`); - lines.push(""); - lines.push("| name | bytes | gzip |"); - lines.push("| --- | --- | --- |"); + lines.push( + `
Unchanged exports (${result.unchanged.length})`, + "", + "| name | bytes | gzip |", + "| --- | --- | --- |", + ); for (const row of result.unchanged) lines.push(`| ${row.name} | ${row.head.bytes} | ${row.head.gzip} |`); - lines.push(""); - lines.push("
"); + lines.push("", "
"); } return lines.join("\n"); }; +const isMeasurement = (value: unknown): value is Measurement => + typeof value === "object" && + value !== null && + "bytes" in value && + typeof value.bytes === "number" && + "gzip" in value && + typeof value.gzip === "number"; + +const isSnapshot = (value: unknown): value is Snapshot => + typeof value === "object" && + value !== null && + "full" in value && + isMeasurement(value.full) && + "exports" in value && + isRecord(value.exports) && + Object.values(value.exports).every((entry) => isMeasurement(entry)); + const main = async (): Promise => { const args = parseArgs(process.argv.slice(2)); - const packageRoot = args.dist ? resolve(process.cwd(), args.dist) : rootDir; + const packageRoot = + args.dist === undefined || args.dist === "" ? rootDir : resolve(process.cwd(), args.dist); const { full, exports: exportsMap, aliasOf, resolveDir } = await measureExports(packageRoot); - if (args.json) { + if (args.json !== undefined && args.json !== "") { const snapshot: Snapshot = { full, exports: exportsMap }; await writeFile(resolve(process.cwd(), args.json), `${JSON.stringify(snapshot, null, "\t")}\n`); } - if (args.compare) { - const base: Snapshot = JSON.parse(await readFile(resolve(process.cwd(), args.compare), "utf8")); + if (args.compare !== undefined && args.compare !== "") { + const comparePath = resolve(process.cwd(), args.compare); + const compareContents = await readFile(comparePath, "utf8"); + const parsedBase: unknown = JSON.parse(compareContents); + + if (!isSnapshot(parsedBase)) { + throw new Error(`${comparePath} is not a valid tree-shaking snapshot`); + } + + const base = parsedBase; const head: Snapshot = { full, exports: exportsMap }; const existingNames = Object.keys(base.exports) .filter((name) => name in exportsMap && !aliasOf.has(name)) @@ -390,7 +451,9 @@ const main = async (): Promise => { const markdown = renderMarkdown(base, head, existing, result); console.log(markdown); - if (args.markdown) await writeFile(resolve(process.cwd(), args.markdown), `${markdown}\n`); + if (args.markdown !== undefined && args.markdown !== "") { + await writeFile(resolve(process.cwd(), args.markdown), `${markdown}\n`); + } if (result.regressions.length > 0 || result.fullImportRegressed) { console.error( diff --git a/src/_internals/clamp-precision/clamp-precision.test.ts b/src/_internals/clamp-precision/clamp-precision.test.ts index 844f8564..24c40fba 100644 --- a/src/_internals/clamp-precision/clamp-precision.test.ts +++ b/src/_internals/clamp-precision/clamp-precision.test.ts @@ -4,13 +4,13 @@ import { clampPrecision } from "./clamp-precision"; describe("clampPrecision", () => { test("should default to 2", () => { expect(clampPrecision()).toBe(2); - expect(clampPrecision(undefined)).toBe(2); + expect(clampPrecision()).toBe(2); }); test("should default to 2 when it is not a finite number", () => { expect(clampPrecision(Number.NaN)).toBe(2); expect(clampPrecision(Number.POSITIVE_INFINITY)).toBe(2); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(clampPrecision("3")).toBe(2); }); diff --git a/src/_internals/crc16-ccitt/crc16-ccitt.ts b/src/_internals/crc16-ccitt/crc16-ccitt.ts index cd858355..30ccac6e 100644 --- a/src/_internals/crc16-ccitt/crc16-ccitt.ts +++ b/src/_internals/crc16-ccitt/crc16-ccitt.ts @@ -1,8 +1,8 @@ -const POLYNOMIAL = 0x1021; +const POLYNOMIAL = 0x10_21; -const INITIAL_VALUE = 0xffff; +const INITIAL_VALUE = 0xff_ff; -const MASK = 0xffff; +const MASK = 0xff_ff; const HEX_LENGTH = 4; @@ -32,7 +32,7 @@ export const crc16Ccitt = (value: string): string => { crc ^= bytes[index] << 8; for (let bit = 0; bit < 8; bit++) { - crc = (crc & 0x8000) === 0 ? (crc << 1) & MASK : ((crc << 1) ^ POLYNOMIAL) & MASK; + crc = (crc & 0x80_00) === 0 ? (crc << 1) & MASK : ((crc << 1) ^ POLYNOMIAL) & MASK; } } diff --git a/src/_internals/fetch-sorted-record/fetch-sorted-record.test.ts b/src/_internals/fetch-sorted-record/fetch-sorted-record.test.ts index d2e916ad..ef4716bb 100644 --- a/src/_internals/fetch-sorted-record/fetch-sorted-record.test.ts +++ b/src/_internals/fetch-sorted-record/fetch-sorted-record.test.ts @@ -15,11 +15,13 @@ describe("fetchSortedRecord", () => { it("should return the parsed entries sorted by key", async () => { globalThis.fetch = vi.fn().mockResolvedValue(new Response("ignored", { status: 200 })); - const sorted = await fetchSortedRecord("https://example.com/table", "Table", async () => ({ - "5102": "Venda", - "1102": "Compra", - "3102": "Compra do exterior", - })); + const sorted = await fetchSortedRecord("https://example.com/table", "Table", () => + Promise.resolve({ + "5102": "Venda", + "1102": "Compra", + "3102": "Compra do exterior", + }), + ); expect(Object.keys(sorted)).toEqual(["1102", "3102", "5102"]); expect(sorted).toEqual({ @@ -32,11 +34,13 @@ describe("fetchSortedRecord", () => { it("should sort non-numeric keys alphabetically", async () => { globalThis.fetch = vi.fn().mockResolvedValue(new Response("ignored", { status: 200 })); - const sorted = await fetchSortedRecord("https://example.com/table", "Table", async () => ({ - banana: "2", - apple: "1", - cherry: "3", - })); + const sorted = await fetchSortedRecord("https://example.com/table", "Table", () => + Promise.resolve({ + banana: "2", + apple: "1", + cherry: "3", + }), + ); expect(Object.keys(sorted)).toEqual(["apple", "banana", "cherry"]); }); @@ -52,6 +56,14 @@ describe("fetchSortedRecord", () => { for (const line of (await response.text()).split("\n")) { const [key, value] = line.split(";"); + + expect(key).toBeDefined(); + expect(value).toBeDefined(); + + if (key === undefined || value === undefined) { + continue; + } + entries[key] = value; } @@ -66,7 +78,7 @@ describe("fetchSortedRecord", () => { globalThis.fetch = vi.fn().mockResolvedValue(new Response("", { status: 503 })); await expect( - fetchSortedRecord("https://example.com/table", "CFOP mirror", async () => ({})), + fetchSortedRecord("https://example.com/table", "CFOP mirror", () => Promise.resolve({})), ).rejects.toThrow("CFOP mirror request failed with status 503"); }); }); diff --git a/src/_internals/fetch-with-retry/fetch-with-retry.test.ts b/src/_internals/fetch-with-retry/fetch-with-retry.test.ts index 87a855ed..c8efbea6 100644 --- a/src/_internals/fetch-with-retry/fetch-with-retry.test.ts +++ b/src/_internals/fetch-with-retry/fetch-with-retry.test.ts @@ -75,9 +75,9 @@ describe("fetchWithRetry", () => { }); it("retries when a top level error code is transient", async () => { - await expectRetrySucceeds( - mockFetchRejectingOnceWith(Object.assign(new Error("boom"), { code: "ECONNRESET" })), - ); + const error = Object.assign(new Error("boom"), { code: "ECONNRESET" }); + + await expectRetrySucceeds(mockFetchRejectingOnceWith(error)); }); it("retries when the error message says fetch failed", async () => { @@ -89,7 +89,9 @@ describe("fetchWithRetry", () => { globalThis.fetch = fetchMock; const rejection = await fetchWithRetry("https://example.com", { retryDelayMs: 0 }).then( - () => undefined, + () => { + throw new Error("expected the fetch to reject"); + }, (error: unknown) => error, ); @@ -102,7 +104,9 @@ describe("fetchWithRetry", () => { globalThis.fetch = fetchMock; const rejection = await fetchWithRetry("https://example.com", { retries: -1 }).then( - () => undefined, + () => { + throw new Error("expected the fetch to reject"); + }, (error: unknown) => error, ); @@ -123,11 +127,13 @@ describe("fetchWithRetry", () => { "ETIMEDOUT", ]; - for (const code of RETRYABLE_CODES) { - await expectRetrySucceeds( - mockFetchRejectingOnceWith(Object.assign(new Error("boom"), { code })), - ); - } + await RETRYABLE_CODES.reduce(async (previous, code) => { + await previous; + + const error = Object.assign(new Error("boom"), { code }); + + await expectRetrySucceeds(mockFetchRejectingOnceWith(error)); + }, Promise.resolve()); }); it("does not retry when the error code is unknown", async () => { @@ -138,11 +144,9 @@ describe("fetchWithRetry", () => { }); it("checks the cause code when the top level code is not a string", async () => { - await expectRetrySucceeds( - mockFetchRejectingOnceWith( - Object.assign(new Error("boom"), { code: 123, cause: { code: "ECONNRESET" } }), - ), - ); + const error = Object.assign(new Error("boom"), { code: 123, cause: { code: "ECONNRESET" } }); + + await expectRetrySucceeds(mockFetchRejectingOnceWith(error)); }); it("propagates the original error when the cause is present but null", async () => { @@ -168,9 +172,9 @@ describe("fetchWithRetry", () => { globalThis.setTimeout = setTimeoutSpy; try { - await expectRetrySucceeds( - mockFetchRejectingOnceWith(Object.assign(new Error("boom"), { code: "ECONNRESET" })), - ); + const error = Object.assign(new Error("boom"), { code: "ECONNRESET" }); + + await expectRetrySucceeds(mockFetchRejectingOnceWith(error)); } finally { globalThis.setTimeout = originalSetTimeout; } diff --git a/src/_internals/fetch-with-retry/fetch-with-retry.ts b/src/_internals/fetch-with-retry/fetch-with-retry.ts index 360e8698..a61f7823 100644 --- a/src/_internals/fetch-with-retry/fetch-with-retry.ts +++ b/src/_internals/fetch-with-retry/fetch-with-retry.ts @@ -7,7 +7,7 @@ export type FetchWithRetryOptions = RequestInit & { retryDelayMs?: number; }; -const RETRYABLE_ERROR_CODES = [ +const RETRYABLE_ERROR_CODES = new Set([ "UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", @@ -17,7 +17,7 @@ const RETRYABLE_ERROR_CODES = [ "EHOSTUNREACH", "ENETUNREACH", "ETIMEDOUT", -]; +]); const getErrorCode = (error: unknown): string | undefined => { if (isNullish(error) || typeof error !== "object") return undefined; @@ -43,7 +43,10 @@ const getErrorCode = (error: unknown): string | undefined => { const isRetryableFetchError = (error: unknown): boolean => { const code = getErrorCode(error); - if (code && RETRYABLE_ERROR_CODES.includes(code)) { + // Stryker disable next-line ConditionalExpression: RETRYABLE_ERROR_CODES.has() only ever + // matches an exact string, so an undefined code reaching that check behaves identically to + // skipping it; the undefined check below exists only to satisfy Set#has's parameter type. + if (code !== undefined && RETRYABLE_ERROR_CODES.has(code)) { return true; } @@ -55,7 +58,36 @@ const isRetryableFetchError = (error: unknown): boolean => { }; const wait = (ms: number): Promise => - ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms)); + ms <= 0 + ? Promise.resolve() + : new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +const attemptFetch = async ( + input: string | URL | Request, + init: RequestInit, + retries: number, + retryDelayMs: number, + attempt: number, + lastError?: unknown, +): Promise => { + if (attempt > retries) { + throw lastError; + } + + try { + return await fetch(input, init); + } catch (error) { + if (attempt === retries || !isRetryableFetchError(error)) { + throw error; + } + + await wait(retryDelayMs * (attempt + 1)); + + return attemptFetch(input, init, retries, retryDelayMs, attempt + 1, error); + } +}; /** * Performs a `fetch` retrying transient network failures with a linear backoff. @@ -73,25 +105,7 @@ const wait = (ms: number): Promise => * await fetchWithRetry("https://viacep.com.br/ws/01001000/json/", { retries: 1, retryDelayMs: 0 }); * ``` */ -export const fetchWithRetry = async ( +export const fetchWithRetry = ( input: string | URL | Request, { retries = 2, retryDelayMs = 250, ...init }: FetchWithRetryOptions = {}, -): Promise => { - let lastError: unknown; - - for (let attempt = 0; attempt <= retries; attempt += 1) { - try { - return await fetch(input, init); - } catch (error) { - lastError = error; - - if (attempt === retries || !isRetryableFetchError(error)) { - throw error; - } - - await wait(retryDelayMs * (attempt + 1)); - } - } - - throw lastError; -}; +): Promise => attemptFetch(input, init, retries, retryDelayMs, 0); diff --git a/src/_internals/format/format.ts b/src/_internals/format/format.ts index d3219e76..c720a849 100644 --- a/src/_internals/format/format.ts +++ b/src/_internals/format/format.ts @@ -34,18 +34,19 @@ export type FormatParams = { export const format = ({ pad, value, pattern }: FormatParams): string => { let formatted = ""; let valueIndex = 0; + let paddedValue = value; - if (pad) { - const separatorsLength = pattern.replace(/[0*]/g, "").length; - value = value.padStart(pattern.length - separatorsLength, "0"); + if (pad === true) { + const separatorsLength = pattern.replaceAll(/[0*]/g, "").length; + paddedValue = value.padStart(pattern.length - separatorsLength, "0"); } for (const char of pattern) { if (char === "0" || char === "*") { - if (valueIndex >= value.length) break; - formatted += char === "*" ? "*" : value[valueIndex]; + if (valueIndex >= paddedValue.length) break; + formatted += char === "*" ? "*" : paddedValue[valueIndex]; valueIndex++; - } else if (valueIndex < value.length) { + } else if (valueIndex < paddedValue.length) { formatted += char; } } diff --git a/src/_internals/generate-checksum/generate-checksum.ts b/src/_internals/generate-checksum/generate-checksum.ts index 616f1b30..641eb788 100644 --- a/src/_internals/generate-checksum/generate-checksum.ts +++ b/src/_internals/generate-checksum/generate-checksum.ts @@ -1,11 +1,11 @@ import { sanitizeToDigits } from "../sanitize-to-digits/sanitize-to-digits"; -export interface GenerateChecksumParams { +export type GenerateChecksumParams = { /** The digits the checksum is computed over. */ base: string | number; /** A starting weight that decreases along the digits, or the explicit weight of each digit. */ weight: number | number[]; -} +}; /** * Sums every digit of a base value multiplied by its weight, the shared first step of the diff --git a/src/_internals/is-nullish/is-nullish.test.ts b/src/_internals/is-nullish/is-nullish.test.ts index ef43b744..5c928a19 100644 --- a/src/_internals/is-nullish/is-nullish.test.ts +++ b/src/_internals/is-nullish/is-nullish.test.ts @@ -3,8 +3,10 @@ import { isNullish } from "./is-nullish"; describe("isNullish", () => { test("should return true for null and undefined", () => { + const isNullishWithoutArgument = isNullish as unknown as () => boolean; + expect(isNullish(null)).toBe(true); - expect(isNullish(undefined)).toBe(true); + expect(isNullishWithoutArgument()).toBe(true); }); test("should return false for every other value, including falsy ones", () => { diff --git a/src/_internals/is-repeated-digits/is-repeated-digits.ts b/src/_internals/is-repeated-digits/is-repeated-digits.ts index bb77a94a..b50912c6 100644 --- a/src/_internals/is-repeated-digits/is-repeated-digits.ts +++ b/src/_internals/is-repeated-digits/is-repeated-digits.ts @@ -11,5 +11,8 @@ * isRepeatedDigits(""); // false * ``` */ -export const isRepeatedDigits = (value: string): boolean => - value.length > 0 && value === value[0].repeat(value.length); +export const isRepeatedDigits = (value: string): boolean => { + const firstChar = value[0]; + + return firstChar !== undefined && value === firstChar.repeat(value.length); +}; diff --git a/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.test.ts b/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.test.ts index 3284537e..5e8bd751 100644 --- a/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.test.ts +++ b/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.test.ts @@ -4,17 +4,17 @@ import { isValidCeiCnoNumber } from "./is-valid-cei-cno-number"; describe("isValidCeiCnoNumber", () => { describe("should return false", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCeiCnoNumber(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCeiCnoNumber(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCeiCnoNumber()).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCeiCnoNumber(true)).toBe(false); }); @@ -76,11 +76,11 @@ describe("isValidCeiCnoNumber", () => { test("for 401800097960, whose check digit is 0 (Receita Federal CNO open dataset, Frutal/MG)", () => { expect(isValidCeiCnoNumber("401800097960")).toBe(true); - expect(isValidCeiCnoNumber(401800097960)).toBe(true); + expect(isValidCeiCnoNumber(401_800_097_960)).toBe(true); }); test("for a number input", () => { - expect(isValidCeiCnoNumber(249859674386)).toBe(true); + expect(isValidCeiCnoNumber(249_859_674_386)).toBe(true); }); test("for a whitespace mask and surrounding whitespace", () => { diff --git a/src/_internals/is-valid-pix-url/is-valid-pix-url.ts b/src/_internals/is-valid-pix-url/is-valid-pix-url.ts index 59448200..07dd4cbf 100644 --- a/src/_internals/is-valid-pix-url/is-valid-pix-url.ts +++ b/src/_internals/is-valid-pix-url/is-valid-pix-url.ts @@ -10,6 +10,8 @@ const PIX_URL_REGEX = new RegExp( * a host name with at least one dot, optionally followed by a path, written without a scheme, * whitespace or characters outside the URL unreserved and sub-delimiter sets. * + * @param {string} value - The value to check. + * @returns {boolean} True if `value` is a valid Pix PSP location. * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf */ export const isValidPixUrl = (value: string): boolean => PIX_URL_REGEX.test(value); diff --git a/src/_internals/mod11/mod11.test.ts b/src/_internals/mod11/mod11.test.ts index ddccda3c..7361b78a 100644 --- a/src/_internals/mod11/mod11.test.ts +++ b/src/_internals/mod11/mod11.test.ts @@ -4,7 +4,7 @@ import { mod11 } from "./mod11"; const mod11Arrecadacao = (value: string) => mod11(value, { variant: "arrecadacao" }); const mod11Bank = (value: string, maxWeight?: number) => - mod11(value, { variant: "bank", maxWeight }); + mod11(value, maxWeight === undefined ? { variant: "bank" } : { variant: "bank", maxWeight }); describe("mod11", () => { describe("default variant (boleto)", () => { diff --git a/src/_internals/mod11/mod11.ts b/src/_internals/mod11/mod11.ts index e847dd0a..8866fb93 100644 --- a/src/_internals/mod11/mod11.ts +++ b/src/_internals/mod11/mod11.ts @@ -46,6 +46,5 @@ export const mod11 = (value: string, options?: Mod11Options): number => { } const remainder = sum % 11; - return remainder in overrides ? overrides[remainder] : 11 - remainder; }; diff --git a/src/_internals/number-to-words/number-to-words.ts b/src/_internals/number-to-words/number-to-words.ts index 2cf7a182..3a11ac9f 100644 --- a/src/_internals/number-to-words/number-to-words.ts +++ b/src/_internals/number-to-words/number-to-words.ts @@ -122,11 +122,12 @@ export const numberToWords = (value: number, options?: NumberToWordsOptions): st let result = ""; - groups.forEach((groupValue, index) => { - if (groupValue === 0) return; + for (const [index, groupValue] of groups.entries()) { + if (groupValue === 0) continue; const scale = highestScale - index; const scaleWord = SCALE_WORDS[scale]; + const groupGender = scale >= 2 ? undefined : gender; const groupText = @@ -138,7 +139,7 @@ export const numberToWords = (value: number, options?: NumberToWordsOptions): st if (result === "") { result = groupText; - return; + continue; } const connector = @@ -146,7 +147,7 @@ export const numberToWords = (value: number, options?: NumberToWordsOptions): st index === lastNonZeroIndex && (groupValue < 100 || isRoundHundred(groupValue)) ? " e " : ", "; result += connector + groupText; - }); + } return result; }; diff --git a/src/_internals/parse-arrecadacao/parse-arrecadacao.test.ts b/src/_internals/parse-arrecadacao/parse-arrecadacao.test.ts index 91b1a1bd..33b33efb 100644 --- a/src/_internals/parse-arrecadacao/parse-arrecadacao.test.ts +++ b/src/_internals/parse-arrecadacao/parse-arrecadacao.test.ts @@ -72,7 +72,7 @@ describe("parseArrecadacao", () => { barcode: MOD11_BARCODE, segment: 5, hasEffectiveValue: true, - amount: 4605246, + amount: 4_605_246, }); }); diff --git a/src/_internals/parse-decimal/parse-decimal.test.ts b/src/_internals/parse-decimal/parse-decimal.test.ts index dc6e6311..13b1b58c 100644 --- a/src/_internals/parse-decimal/parse-decimal.test.ts +++ b/src/_internals/parse-decimal/parse-decimal.test.ts @@ -11,7 +11,7 @@ describe("parseDecimal", () => { test("should read a long run after the separator as thousands", () => { expect(parseDecimal("R$ 1.234")).toBe(1234); - expect(parseDecimal("1.000.000")).toBe(1000000); + expect(parseDecimal("1.000.000")).toBe(1_000_000); }); test("should read a value without separators as whole units by default", () => { @@ -46,7 +46,7 @@ describe("parseDecimal", () => { test("should return 0 when there is nothing to read", () => { expect(parseDecimal("")).toBe(0); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parseDecimal(null)).toBe(0); expect(parseDecimal("R$")).toBe(0); expect(parseDecimal("-")).toBe(0); diff --git a/src/_internals/parse-decimal/parse-decimal.ts b/src/_internals/parse-decimal/parse-decimal.ts index a54c145c..d36d72da 100644 --- a/src/_internals/parse-decimal/parse-decimal.ts +++ b/src/_internals/parse-decimal/parse-decimal.ts @@ -37,7 +37,7 @@ export const parseDecimal = (value: string, options?: ParseDecimalOptions): numb const [prefix] = value.split(/\d/, 1); const sign = prefix.includes("-") ? -1 : 1; - const cleaned = value.replace(/[^\d.,]/g, ""); + const cleaned = value.replaceAll(/[^\d.,]/g, ""); const separatorIndex = Math.max(cleaned.lastIndexOf(","), cleaned.lastIndexOf(".")); if (separatorIndex === -1) { @@ -46,7 +46,10 @@ export const parseDecimal = (value: string, options?: ParseDecimalOptions): numb const fraction = cleaned.slice(separatorIndex + 1); const isDecimal = fraction.length <= maxFractionDigits; - const integerPart = (isDecimal ? cleaned.slice(0, separatorIndex) : cleaned).replace(/\D/g, ""); + const integerPart = (isDecimal ? cleaned.slice(0, separatorIndex) : cleaned).replaceAll( + /\D/g, + "", + ); const numeric = isDecimal ? `${integerPart}.${fraction}` : integerPart; return sign * Number.parseFloat(numeric) || 0; diff --git a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.test.ts b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.test.ts index ade11fed..514549b9 100644 --- a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.test.ts +++ b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.test.ts @@ -7,6 +7,6 @@ describe("sanitizeToAlphanumeric", () => { }); it("should support number input", () => { - expect(sanitizeToAlphanumeric(12345)).toBe("12345"); + expect(sanitizeToAlphanumeric(12_345)).toBe("12345"); }); }); diff --git a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts index 3c88196e..ccf97d62 100644 --- a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts +++ b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts @@ -14,5 +14,5 @@ export const sanitizeToAlphanumeric = (value: string | number): string => value .toString() - .replace(/[^A-Za-z0-9]/g, "") + .replaceAll(/[^A-Za-z0-9]/g, "") .toUpperCase(); diff --git a/src/_internals/sanitize-to-ascii/sanitize-to-ascii.ts b/src/_internals/sanitize-to-ascii/sanitize-to-ascii.ts index d819f71b..3d366acf 100644 --- a/src/_internals/sanitize-to-ascii/sanitize-to-ascii.ts +++ b/src/_internals/sanitize-to-ascii/sanitize-to-ascii.ts @@ -1,6 +1,6 @@ -const COMBINING_MARKS_REGEX = /[\u0300-\u036f]/g; +const COMBINING_MARKS_REGEX = /[\u0300-\u036F]/g; -const NON_PRINTABLE_ASCII_REGEX = /[^\u0020-\u007e]/g; +const NON_PRINTABLE_ASCII_REGEX = /[^\u0020-\u007E]/g; const WHITESPACE_REGEX = /\s+/g; diff --git a/src/_internals/sanitize-to-digits/sanitize-to-digits.test.ts b/src/_internals/sanitize-to-digits/sanitize-to-digits.test.ts index e56691e6..e1c46d82 100644 --- a/src/_internals/sanitize-to-digits/sanitize-to-digits.test.ts +++ b/src/_internals/sanitize-to-digits/sanitize-to-digits.test.ts @@ -19,7 +19,7 @@ describe("sanitizeToDigits", () => { }); it("should handle a number input", () => { - expect(sanitizeToDigits(123456)).toBe("123456"); + expect(sanitizeToDigits(123_456)).toBe("123456"); }); it("should handle a number with non-digit characters", () => { diff --git a/src/_internals/sanitize-to-digits/sanitize-to-digits.ts b/src/_internals/sanitize-to-digits/sanitize-to-digits.ts index f0bcb62e..4bb380c2 100644 --- a/src/_internals/sanitize-to-digits/sanitize-to-digits.ts +++ b/src/_internals/sanitize-to-digits/sanitize-to-digits.ts @@ -13,4 +13,4 @@ * ``` */ export const sanitizeToDigits = (value: string | number): string => - value.toString().replace(/\D/g, ""); + value.toString().replaceAll(/\D/g, ""); diff --git a/src/_internals/test/globals.d.ts b/src/_internals/test/globals.d.ts index 0b93aa5c..f28a888f 100644 --- a/src/_internals/test/globals.d.ts +++ b/src/_internals/test/globals.d.ts @@ -14,11 +14,18 @@ declare const Deno: { declare global { var RUN_LIVE_CEP_TESTS: string | number | undefined; - interface GlobalThis { - RUN_LIVE_CEP_TESTS?: string | number; - } } +type BunJestMockFunction = ((...args: unknown[]) => unknown) & { + mock: { calls: unknown[][] }; + mockClear: () => void; + mockResolvedValue: (value: unknown) => BunJestMockFunction; + mockResolvedValueOnce: (value: unknown) => BunJestMockFunction; + mockRejectedValue: (value: unknown) => BunJestMockFunction; + mockRejectedValueOnce: (value: unknown) => BunJestMockFunction; + mockImplementation: (implementation: (...args: unknown[]) => unknown) => BunJestMockFunction; +}; + declare module "bun:test" { export const describe: any; export const it: any; @@ -26,5 +33,8 @@ declare module "bun:test" { export const test: any; export const beforeEach: any; export const afterEach: any; - export const jest: any; + export const jest: { + fn: (implementation?: (...args: unknown[]) => unknown) => BunJestMockFunction; + restoreAllMocks: () => void; + }; } diff --git a/src/_internals/test/runtime-bun.ts b/src/_internals/test/runtime-bun.ts index 2201869e..c1da6d3f 100644 --- a/src/_internals/test/runtime-bun.ts +++ b/src/_internals/test/runtime-bun.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it, jest, test } from "bun:test"; +import { jest } from "bun:test"; -export { afterEach, beforeEach, describe, expect, it, test }; +export { afterEach, beforeEach, describe, expect, it, test } from "bun:test"; export const vi = { fn: jest.fn, - restoreAllMocks: () => { + restoreAllMocks: (): void => { jest.restoreAllMocks(); }, }; diff --git a/src/_internals/test/runtime-deno.ts b/src/_internals/test/runtime-deno.ts index 6f35cf92..ac756e51 100644 --- a/src/_internals/test/runtime-deno.ts +++ b/src/_internals/test/runtime-deno.ts @@ -32,7 +32,7 @@ function hasLength(value: unknown): value is { length: number } { return true; } - return isRecord(value) && typeof value.length === "number"; + return isRecord(value) && typeof value["length"] === "number"; } function createAssertionError(message: string): Error { @@ -99,6 +99,7 @@ function objectMatches( function createMock(implementation?: MockImplementation): MockFunction { const queue: MockImplementation[] = []; const calls: unknown[][] = []; + let currentImplementation = implementation; const baseFn = (...args: unknown[]): unknown => { calls.push(args); @@ -106,11 +107,13 @@ function createMock(implementation?: MockImplementation): MockFunction { if (queue.length > 0) { const nextImplementation = queue.shift(); - return nextImplementation!(...args); + if (nextImplementation !== undefined) { + return nextImplementation(...args); + } } - if (implementation) { - return implementation(...args); + if (currentImplementation) { + return currentImplementation(...args); } return undefined; @@ -118,32 +121,40 @@ function createMock(implementation?: MockImplementation): MockFunction { const mockFn: MockFunction = Object.assign(baseFn, { mock: { calls }, - mockClear: () => { + mockClear: (): void => { queue.length = 0; calls.length = 0; }, - mockResolvedValueOnce: (value: unknown) => { + mockResolvedValueOnce: (value: unknown): MockFunction => { queue.push(() => Promise.resolve(value)); return mockFn; }, - mockRejectedValueOnce: (value: unknown) => { - queue.push(() => Promise.reject(value)); + mockRejectedValueOnce: (value: unknown): MockFunction => { + queue.push(async () => { + await Promise.resolve(); + + throw value; + }); return mockFn; }, - mockResolvedValue: (value: unknown) => { - implementation = () => Promise.resolve(value); + mockResolvedValue: (value: unknown): MockFunction => { + currentImplementation = (): Promise => Promise.resolve(value); return mockFn; }, - mockRejectedValue: (value: unknown) => { - implementation = () => Promise.reject(value); + mockRejectedValue: (value: unknown): MockFunction => { + currentImplementation = async (): Promise => { + await Promise.resolve(); + + throw value; + }; return mockFn; }, - mockImplementation: (nextImplementation: MockImplementation) => { - implementation = nextImplementation; + mockImplementation: (nextImplementation: MockImplementation): MockFunction => { + currentImplementation = nextImplementation; return mockFn; }, @@ -197,185 +208,214 @@ function isMockFunction(value: unknown): value is MockFunction { return false; } - return isRecord(value.mock) && Array.isArray(value.mock.calls); + return isRecord(value.mock) && Array.isArray(value.mock["calls"]); } -function createMatchers(actual: unknown) { - return { - toBe(expected: unknown) { - if (!Object.is(actual, expected)) { - throw createAssertionError(`Expected ${String(actual)} to be ${String(expected)}`); - } - }, - toEqual(expected: unknown) { - if (!deepEqual(actual, expected)) { - throw createAssertionError("Expected values to be deeply equal"); - } - }, - toStrictEqual(expected: unknown) { - if (!deepEqual(actual, expected)) { - throw createAssertionError("Expected values to be strictly equal"); - } - }, - toContain(expected: unknown) { - if (typeof actual === "string") { - if (!actual.includes(String(expected))) { - throw createAssertionError(`Expected ${actual} to contain ${String(expected)}`); - } +type Matcher = (...args: any[]) => void; - return; - } +type Matchers = Record; - if (!Array.isArray(actual) || !actual.includes(expected)) { - throw createAssertionError(`Expected value to contain ${String(expected)}`); - } - }, - toMatch(expected: RegExp | string) { - if (typeof actual !== "string") { - throw createAssertionError("Expected value to be a string"); - } +const isCallable = (value: unknown): value is (...args: unknown[]) => unknown => + typeof value === "function"; - if (expected instanceof RegExp) { - if (!expected.test(actual)) { - throw createAssertionError(`Expected ${actual} to match ${String(expected)}`); - } +const createEqualityMatchers = (actual: unknown): Matchers => ({ + toBe(expected: unknown): void { + if (!Object.is(actual, expected)) { + throw createAssertionError(`Expected ${String(actual)} to be ${String(expected)}`); + } + }, + toEqual(expected: unknown): void { + if (!deepEqual(actual, expected)) { + throw createAssertionError("Expected values to be deeply equal"); + } + }, + toStrictEqual(expected: unknown): void { + if (!deepEqual(actual, expected)) { + throw createAssertionError("Expected values to be strictly equal"); + } + }, + toBeDefined(): void { + if (actual === undefined || actual === null) { + throw createAssertionError("Expected value to be defined"); + } + }, + toBeUndefined(): void { + if (actual !== undefined) { + throw createAssertionError(`Expected ${describeValue(actual)} to be undefined`); + } + }, + toBeTruthy(): void { + const isTruthy = Boolean(actual); - return; - } + if (isTruthy) return; - if (!actual.includes(expected)) { - throw createAssertionError(`Expected ${actual} to contain ${expected}`); - } - }, - toContainEqual(expected: unknown) { - if (!Array.isArray(actual)) { - throw createAssertionError("Expected value to be an array"); - } + throw createAssertionError(`Expected ${String(actual)} to be truthy`); + }, + toBeNull(): void { + if (actual !== null) { + throw createAssertionError(`Expected ${describeValue(actual)} to be null`); + } + }, + toBeInstanceOf(expected: new (...args: any[]) => unknown): void { + if (!(actual instanceof expected)) { + throw createAssertionError(`Expected value to be instance of ${expected.name}`); + } + }, +}); - if (!actual.some((value) => deepEqual(value, expected))) { - throw createAssertionError("Expected array to contain a deeply equal value"); - } - }, - toHaveProperty(property: string) { - if (!isRecord(actual) || !(property in actual)) { - throw createAssertionError(`Expected object to have property ${property}`); - } - }, - toHaveLength(expected: number) { - if (!hasLength(actual)) { - throw createAssertionError("Expected value to have a length"); - } +const createComparisonMatchers = (actual: unknown): Matchers => ({ + toBeGreaterThan(expected: number): void { + if (!(typeof actual === "number" && actual > expected)) { + throw createAssertionError(`Expected ${String(actual)} to be greater than ${expected}`); + } + }, + toBeGreaterThanOrEqual(expected: number): void { + if (!(typeof actual === "number" && actual >= expected)) { + throw createAssertionError( + `Expected ${String(actual)} to be greater than or equal to ${expected}`, + ); + } + }, + toBeLessThanOrEqual(expected: number): void { + if (!(typeof actual === "number" && actual <= expected)) { + throw createAssertionError( + `Expected ${String(actual)} to be less than or equal to ${expected}`, + ); + } + }, + toBeLessThan(expected: number): void { + if (!(typeof actual === "number" && actual < expected)) { + throw createAssertionError(`Expected ${String(actual)} to be less than ${expected}`); + } + }, +}); - if (actual.length !== expected) { - throw createAssertionError(`Expected length ${actual.length} to be ${expected}`); - } - }, - toBeDefined() { - if (actual === undefined || actual === null) { - throw createAssertionError("Expected value to be defined"); - } - }, - toBeUndefined() { - if (actual !== undefined) { - throw createAssertionError(`Expected ${describeValue(actual)} to be undefined`); - } - }, - toBeTruthy() { - if (!actual) { - throw createAssertionError(`Expected ${String(actual)} to be truthy`); - } - }, - toBeInstanceOf(expected: new (...args: any[]) => unknown) { - if (!(actual instanceof expected)) { - throw createAssertionError(`Expected value to be instance of ${expected.name}`); - } - }, - toBeGreaterThan(expected: number) { - if (!(typeof actual === "number" && actual > expected)) { - throw createAssertionError(`Expected ${String(actual)} to be greater than ${expected}`); - } - }, - toBeGreaterThanOrEqual(expected: number) { - if (!(typeof actual === "number" && actual >= expected)) { - throw createAssertionError( - `Expected ${String(actual)} to be greater than or equal to ${expected}`, - ); - } - }, - toBeLessThanOrEqual(expected: number) { - if (!(typeof actual === "number" && actual <= expected)) { - throw createAssertionError( - `Expected ${String(actual)} to be less than or equal to ${expected}`, - ); - } - }, - toBeNull() { - if (actual !== null) { - throw createAssertionError(`Expected ${describeValue(actual)} to be null`); - } - }, - toBeLessThan(expected: number) { - if (!(typeof actual === "number" && actual < expected)) { - throw createAssertionError(`Expected ${String(actual)} to be less than ${expected}`); - } - }, - toThrow(expected?: ThrowExpectation) { - if (typeof actual !== "function") { - throw createAssertionError("Expected value to be a function"); +const createCollectionMatchers = (actual: unknown): Matchers => ({ + toContain(expected: unknown): void { + if (typeof actual === "string") { + if (!actual.includes(String(expected))) { + throw createAssertionError(`Expected ${actual} to contain ${String(expected)}`); } - try { - actual(); - } catch (error) { - assertThrown(error, expected); + return; + } - return; - } + if (!Array.isArray(actual) || !actual.includes(expected)) { + throw createAssertionError(`Expected value to contain ${String(expected)}`); + } + }, + toMatch(expected: RegExp | string): void { + if (typeof actual !== "string") { + throw createAssertionError("Expected value to be a string"); + } - throw createAssertionError("Expected function to throw"); - }, - toHaveBeenCalled() { - if (!isMockFunction(actual)) { - throw createAssertionError("Expected value to be a mock function"); + if (expected instanceof RegExp) { + if (!expected.test(actual)) { + throw createAssertionError(`Expected ${actual} to match ${String(expected)}`); } - if (actual.mock.calls.length === 0) { - throw createAssertionError("Expected mock function to have been called"); - } - }, - toHaveBeenCalledTimes(expected: number) { - if (!isMockFunction(actual)) { - throw createAssertionError("Expected value to be a mock function"); - } + return; + } - if (actual.mock.calls.length !== expected) { - throw createAssertionError( - `Expected mock function to have been called ${expected} times, but it was called ${actual.mock.calls.length} times`, - ); - } - }, - toMatchObject(expected: Record) { - if (!isRecord(actual) || !objectMatches(actual, expected)) { - throw createAssertionError("Expected object to match"); - } - }, + if (!actual.includes(expected)) { + throw createAssertionError(`Expected ${actual} to contain ${expected}`); + } + }, + toContainEqual(expected: unknown): void { + if (!Array.isArray(actual)) { + throw createAssertionError("Expected value to be an array"); + } + + if (!actual.some((value) => deepEqual(value, expected))) { + throw createAssertionError("Expected array to contain a deeply equal value"); + } + }, + toHaveProperty(property: string): void { + if (!isRecord(actual) || !(property in actual)) { + throw createAssertionError(`Expected object to have property ${property}`); + } + }, + toHaveLength(expected: number): void { + if (!hasLength(actual)) { + throw createAssertionError("Expected value to have a length"); + } + + if (actual.length !== expected) { + throw createAssertionError(`Expected length ${actual.length} to be ${expected}`); + } + }, + toMatchObject(expected: Record): void { + if (!isRecord(actual) || !objectMatches(actual, expected)) { + throw createAssertionError("Expected object to match"); + } + }, +}); + +const createBehaviorMatchers = (actual: unknown): Matchers => ({ + toThrow(expected?: ThrowExpectation): void { + if (!isCallable(actual)) { + throw createAssertionError("Expected value to be a function"); + } + + try { + actual(); + } catch (error) { + assertThrown(error, expected); + + return; + } + + throw createAssertionError("Expected function to throw"); + }, + toHaveBeenCalled(): void { + if (!isMockFunction(actual)) { + throw createAssertionError("Expected value to be a mock function"); + } + + if (actual.mock.calls.length === 0) { + throw createAssertionError("Expected mock function to have been called"); + } + }, + toHaveBeenCalledTimes(expected: number): void { + if (!isMockFunction(actual)) { + throw createAssertionError("Expected value to be a mock function"); + } + + if (actual.mock.calls.length !== expected) { + throw createAssertionError( + `Expected mock function to have been called ${expected} times, but it was called ${actual.mock.calls.length} times`, + ); + } + }, +}); + +function createMatchers(actual?: unknown): Matchers { + return { + ...createEqualityMatchers(actual), + ...createComparisonMatchers(actual), + ...createCollectionMatchers(actual), + ...createBehaviorMatchers(actual), }; } -function createExpect(actual: unknown) { +type ExpectResult = Record & { + readonly not: Matchers; + readonly resolves: Record Promise>; + readonly rejects: { toThrow: (expected?: ThrowExpectation) => Promise }; +}; + +function createExpect(actual: unknown): ExpectResult { const matchers = createMatchers(actual); return { ...matchers, - get not() { + get not(): Matchers { return Object.fromEntries( Object.entries(matchers).map(([name, matcher]) => [ name, - (...args: unknown[]) => { - const fn: Function = matcher; - + (...args: unknown[]): void => { try { - fn.apply(undefined, args); + matcher.apply(undefined, args); } catch { return; } @@ -385,28 +425,28 @@ function createExpect(actual: unknown) { ]), ); }, - get resolves() { + get resolves(): Record Promise> { const promise = Promise.resolve(actual); return Object.fromEntries( - Object.entries(createMatchers(undefined)).map(([name]) => [ + Object.entries(createMatchers()).map(([name]) => [ name, - async (...args: unknown[]) => { + async (...args: unknown[]): Promise => { const resolved = await promise; const resolvedMatchers = createMatchers(resolved); const matcherEntry = Object.entries(resolvedMatchers).find( ([entryName]) => entryName === name, ); - const fn: Function | undefined = matcherEntry?.[1]; + const matcher = matcherEntry?.[1]; - return fn?.apply(undefined, args); + return matcher?.apply(undefined, args); }, ]), ); }, - get rejects() { + get rejects(): { toThrow: (expected?: ThrowExpectation) => Promise } { return { - async toThrow(expected?: ThrowExpectation) { + async toThrow(expected?: ThrowExpectation): Promise { try { await actual; } catch (error) { @@ -422,13 +462,16 @@ function createExpect(actual: unknown) { }; } -async function runHooks(hooks: TestCallback[]) { - for (const hook of hooks) { - await hook(); - } +async function runHooks(hooks: TestCallback[]): Promise { + const [hook, ...rest] = hooks; + + if (hook === undefined) return; + + await hook(); + await runHooks(rest); } -function currentSuiteChain() { +function currentSuiteChain(): Suite[] { return [...suiteStack]; } @@ -438,7 +481,7 @@ type DescribeFunction = ((name: string, callback: TestCallback) => void) & { let skipDepth = 0; -const runSuite = (name: string, callback: TestCallback) => { +const runSuite = (name: string, callback: TestCallback): void => { suiteStack.push({ afterEach: [], beforeEach: [], @@ -446,17 +489,19 @@ const runSuite = (name: string, callback: TestCallback) => { }); try { - void callback(); + Promise.resolve(callback()).catch((error: unknown) => { + throw error; + }); } finally { suiteStack.pop(); } }; -const describe: DescribeFunction = (name, callback) => { +const describe: DescribeFunction = (name, callback): void => { runSuite(name, callback); }; -describe.skip = (name, callback) => { +describe.skip = (name, callback): void => { skipDepth += 1; try { @@ -466,7 +511,7 @@ describe.skip = (name, callback) => { } }; -export function beforeEach(callback: TestCallback) { +export function beforeEach(callback: TestCallback): void { const currentSuite = suiteStack.at(-1); if (!currentSuite) { @@ -476,7 +521,7 @@ export function beforeEach(callback: TestCallback) { currentSuite.beforeEach.push(callback); } -export function afterEach(callback: TestCallback) { +export function afterEach(callback: TestCallback): void { const currentSuite = suiteStack.at(-1); if (!currentSuite) { @@ -486,7 +531,7 @@ export function afterEach(callback: TestCallback) { currentSuite.afterEach.push(callback); } -export function it(name: string, callback: TestCallback, timeout?: number) { +export function it(name: string, callback: TestCallback, timeout?: number): void { const suites = currentSuiteChain(); const testName = [...suites.map((suite) => suite.name), name].join(" > "); @@ -504,7 +549,7 @@ export function it(name: string, callback: TestCallback, timeout?: number) { name: testName, sanitizeOps: false, sanitizeResources: false, - ...(timeout ? { sanitizeExit: false } : {}), + ...(timeout !== undefined && timeout !== 0 ? { sanitizeExit: false } : {}), }); } @@ -514,7 +559,7 @@ export const expect = createExpect; export const vi = { fn: createMock, - restoreAllMocks: () => { + restoreAllMocks: (): void => { for (const mockFn of registeredMocks) { mockFn.mockClear(); } diff --git a/src/_internals/test/runtime.ts b/src/_internals/test/runtime.ts index 39c0682f..cb8960c3 100644 --- a/src/_internals/test/runtime.ts +++ b/src/_internals/test/runtime.ts @@ -1,4 +1,16 @@ -const runtimeModule = +type RuntimeModule = { + afterEach: (callback: () => void | Promise) => void; + beforeEach: (callback: () => void | Promise) => void; + describe: ((name: string, callback: () => void) => void) & { + skip: (name: string, callback: () => void) => void; + }; + expect: (actual: unknown) => any; + it: (name: string, callback: () => void | Promise, timeout?: number) => void; + test: (name: string, callback: () => void | Promise, timeout?: number) => void; + vi: { fn: (...args: any[]) => any; restoreAllMocks: () => void }; +}; + +const runtimeModule: RuntimeModule = "Bun" in globalThis ? await import("./runtime-bun") : "Deno" in globalThis diff --git a/src/add-business-days/add-business-days.test.ts b/src/add-business-days/add-business-days.test.ts index e3366c4e..d2f3555e 100644 --- a/src/add-business-days/add-business-days.test.ts +++ b/src/add-business-days/add-business-days.test.ts @@ -132,22 +132,22 @@ describe("addBusinessDays", () => { describe("invalid input", () => { it("should return null when params is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(addBusinessDays(null)).toBeNull(); }); it("should return null when params is undefined", () => { - // @ts-expect-error - expect(addBusinessDays(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(addBusinessDays()).toBeNull(); }); it("should return null when params is not an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(addBusinessDays("2024-01-02")).toBeNull(); }); it('should return null when params is a function, even one carrying date/days properties (typeof params !== "object" must reject it, not just isNullish)', () => { - const fakeParams = Object.assign(() => {}, { date: new Date(2024, 0, 2), days: 1 }); + const fakeParams = Object.assign(() => null, { date: new Date(2024, 0, 2), days: 1 }); expect(addBusinessDays(fakeParams)).toBeNull(); }); @@ -157,7 +157,7 @@ describe("addBusinessDays", () => { }); it("should return null when date is not a Date", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(addBusinessDays({ date: "2024-01-02", days: 1 })).toBeNull(); }); @@ -176,13 +176,13 @@ describe("addBusinessDays", () => { }); it("should return null when days is not a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(addBusinessDays({ date: new Date(2024, 0, 2), days: "1" })).toBeNull(); }); it("should return null when stateCode is not a string", () => { expect( - // @ts-expect-error + // @ts-expect-error: intentionally invalid input addBusinessDays({ date: new Date(2024, 0, 2), days: 1, stateCode: 123 }), ).toBeNull(); }); diff --git a/src/capitalize/capitalize.test.ts b/src/capitalize/capitalize.test.ts index eb38c472..964dedc5 100644 --- a/src/capitalize/capitalize.test.ts +++ b/src/capitalize/capitalize.test.ts @@ -73,11 +73,11 @@ describe("capitalize", () => { }); test("should return an empty string when the value is not a string", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(capitalize(null)).toBe(""); - // @ts-expect-error - expect(capitalize(undefined)).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input + expect(capitalize()).toBe(""); + // @ts-expect-error: intentionally invalid input expect(capitalize(123)).toBe(""); }); }); diff --git a/src/capitalize/capitalize.ts b/src/capitalize/capitalize.ts index 29a34c70..346ea951 100644 --- a/src/capitalize/capitalize.ts +++ b/src/capitalize/capitalize.ts @@ -20,11 +20,11 @@ export type CapitalizeOptions = { * comparison ignores the case of the words given in both lists. * - All other words will be capitalized (first letter upper case, rest lower case). * - * @param value - The input string to be capitalized. - * @param options - Optional configuration for capitalization. - * @param options.lowerCaseWords - Array of words to keep in lower case (default: `PREPOSITIONS`). - * @param options.upperCaseWords - Array of words to keep in upper case (default: `[]`). - * @returns The capitalized string according to the specified rules. + * @param {string} value - The input string to be capitalized. + * @param {CapitalizeOptions} [options] - Optional configuration for capitalization. + * @param {string[]} [options.lowerCaseWords] - Array of words to keep in lower case (default: `PREPOSITIONS`). + * @param {string[]} [options.upperCaseWords] - Array of words to keep in upper case (default: `[]`). + * @returns {string} The capitalized string according to the specified rules. * * @example * ```typescript diff --git a/src/convert-currency-to-words/convert-currency-to-words.test.ts b/src/convert-currency-to-words/convert-currency-to-words.test.ts index 809e0100..07f7fa8d 100644 --- a/src/convert-currency-to-words/convert-currency-to-words.test.ts +++ b/src/convert-currency-to-words/convert-currency-to-words.test.ts @@ -2,7 +2,7 @@ import { NUMBER_TO_WORDS_MAX_VALUE } from "../_internals/number-to-words/number- import { describe, expect, test } from "../_internals/test/runtime"; import { convertCurrencyToWords } from "./convert-currency-to-words"; -function expectAmounts(cases: ReadonlyArray): void { +function expectAmounts(cases: readonly (readonly [number, string])[]): void { const failures = cases .map(([amount, expected]) => ({ amount, actual: convertCurrencyToWords(amount), expected })) .filter(({ actual, expected }) => actual !== expected); @@ -20,7 +20,7 @@ describe("convertCurrencyToWords", () => { }); test("should return 'um real' for 1.00", () => { - expect(convertCurrencyToWords(1.0)).toBe("um real"); + expect(convertCurrencyToWords(1)).toBe("um real"); }); test("should return 'um real e um centavo' for 1.01", () => { @@ -28,11 +28,11 @@ describe("convertCurrencyToWords", () => { }); test("should insert 'de' before 'reais' for a round million (1000000.00, brutils 'convert_real_to_text')", () => { - expect(convertCurrencyToWords(1000000.0)).toBe("um milhão de reais"); + expect(convertCurrencyToWords(1_000_000)).toBe("um milhão de reais"); }); test("should pluralize the 'de' connector for two round million (2000000.00)", () => { - expect(convertCurrencyToWords(2000000.0)).toBe("dois milhões de reais"); + expect(convertCurrencyToWords(2_000_000)).toBe("dois milhões de reais"); }); test("should join reais and centavos with 'e' (1523.45, brutils 'convert_real_to_text' example)", () => { @@ -42,7 +42,7 @@ describe("convertCurrencyToWords", () => { }); test("should not insert 'de' when a mil/hundred group follows the million group", () => { - expect(convertCurrencyToWords(1000230.0)).toBe("um milhão, duzentos e trinta reais"); + expect(convertCurrencyToWords(1_000_230)).toBe("um milhão, duzentos e trinta reais"); }); test("should return only the centavos when the reais part is zero", () => { @@ -50,7 +50,7 @@ describe("convertCurrencyToWords", () => { }); test("should return only the reais when the centavos part is zero", () => { - expect(convertCurrencyToWords(100.0)).toBe("cem reais"); + expect(convertCurrencyToWords(100)).toBe("cem reais"); }); test("should truncate (not round) to 2 decimal places", () => { @@ -77,12 +77,12 @@ describe("convertCurrencyToWords", () => { }); test("should return '' for a non-number value", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertCurrencyToWords("1523.45")).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertCurrencyToWords(null)).toBe(""); - // @ts-expect-error - expect(convertCurrencyToWords(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(convertCurrencyToWords()).toBe(""); }); }); @@ -140,14 +140,14 @@ describe("convertCurrencyToWords", () => { }); test("should ignore an invalid case value and fall back to 'lower'", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertCurrencyToWords(1000, { case: "invalid" })).toBe("mil reais"); }); }); describe("literal case tables", () => { test("should match a hand-written string for every amount from R$ 0.00 to R$ 1.49, cent by cent", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [0, "zero reais"], [1, "um centavo"], [2, "dois centavos"], @@ -304,68 +304,68 @@ describe("convertCurrencyToWords", () => { }); test("should match a hand-written string at reais boundaries, scale words and truncation cases", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [1000, "mil reais"], [1000.01, "mil reais e um centavo"], [1101, "mil, cento e um reais"], [1101.01, "mil, cento e um reais e um centavo"], [1523.45, "mil, quinhentos e vinte e três reais e quarenta e cinco centavos"], - [1000000, "um milhão de reais"], - [1000000.01, "um milhão de reais e um centavo"], - [2000000, "dois milhões de reais"], - [1000001, "um milhão e um reais"], + [1_000_000, "um milhão de reais"], + [1_000_000.01, "um milhão de reais e um centavo"], + [2_000_000, "dois milhões de reais"], + [1_000_001, "um milhão e um reais"], [ - 999999999999999, + 999_999_999_999_999, "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais", ], [1.999, "um real e noventa e nove centavos"], [100.5, "cem reais e cinquenta centavos"], [2, "dois reais"], [10.5, "dez reais e cinquenta centavos"], - [999999, "novecentos e noventa e nove mil, novecentos e noventa e nove reais"], + [999_999, "novecentos e noventa e nove mil, novecentos e noventa e nove reais"], [100, "cem reais"], - [1000000000, "um bilhão de reais"], - [2000000000, "dois bilhões de reais"], - [1000000000000, "um trilhão de reais"], - [2000000000000, "dois trilhões de reais"], + [1_000_000_000, "um bilhão de reais"], + [2_000_000_000, "dois bilhões de reais"], + [1_000_000_000_000, "um trilhão de reais"], + [2_000_000_000_000, "dois trilhões de reais"], ]; expectAmounts(cases); }); test("should reproduce every published brutils 'convert_real_to_text' example (tests/test_currency.py, lowercase here because brutils capitalizes and this library leaves casing to the caller)", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [0, "zero reais"], [0.01, "um centavo"], [0.5, "cinquenta centavos"], [1, "um real"], [-50.25, "menos cinquenta reais e vinte e cinco centavos"], [1523.45, "mil, quinhentos e vinte e três reais e quarenta e cinco centavos"], - [1000000, "um milhão de reais"], - [2000000, "dois milhões de reais"], - [1000000000, "um bilhão de reais"], - [2000000000, "dois bilhões de reais"], - [1000000000000, "um trilhão de reais"], - [2000000000000, "dois trilhões de reais"], - [1000000.45, "um milhão de reais e quarenta e cinco centavos"], - [2000000000.99, "dois bilhões de reais e noventa e nove centavos"], + [1_000_000, "um milhão de reais"], + [2_000_000, "dois milhões de reais"], + [1_000_000_000, "um bilhão de reais"], + [2_000_000_000, "dois bilhões de reais"], + [1_000_000_000_000, "um trilhão de reais"], + [2_000_000_000_000, "dois trilhões de reais"], + [1_000_000.45, "um milhão de reais e quarenta e cinco centavos"], + [2_000_000_000.99, "dois bilhões de reais e noventa e nove centavos"], [ - 1234567890.5, + 1_234_567_890.5, "um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa reais e cinquenta centavos", ], [0.001, "zero reais"], [0.009, "zero reais"], - [-1000000, "menos um milhão de reais"], - [-2000000.5, "menos dois milhões de reais e cinquenta centavos"], - [1000000000.01, "um bilhão de reais e um centavo"], - [1000000000.99, "um bilhão de reais e noventa e nove centavos"], + [-1_000_000, "menos um milhão de reais"], + [-2_000_000.5, "menos dois milhões de reais e cinquenta centavos"], + [1_000_000_000.01, "um bilhão de reais e um centavo"], + [1_000_000_000.99, "um bilhão de reais e noventa e nove centavos"], [ - 999999999999.99, + 999_999_999_999.99, "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos", ], - [1000000000000.01, "um trilhão de reais e um centavo"], - [1000000000000.99, "um trilhão de reais e noventa e nove centavos"], + [1_000_000_000_000.01, "um trilhão de reais e um centavo"], + [1_000_000_000_000.99, "um trilhão de reais e noventa e nove centavos"], [ - 9999999999999.99, + 9_999_999_999_999.99, "nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos", ], ]; @@ -373,13 +373,13 @@ describe("convertCurrencyToWords", () => { }); test("should prefix 'menos' to a hand-written string for negative amounts", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [-0.01, "menos um centavo"], [-1, "menos um real"], [-1.5, "menos um real e cinquenta centavos"], [-5.5, "menos cinco reais e cinquenta centavos"], [-100, "menos cem reais"], - [-1000000, "menos um milhão de reais"], + [-1_000_000, "menos um milhão de reais"], [-0.001, "zero reais"], [-0.009, "zero reais"], ]; diff --git a/src/convert-date-to-words/convert-date-to-words.test.ts b/src/convert-date-to-words/convert-date-to-words.test.ts index f69934c6..bcb265b2 100644 --- a/src/convert-date-to-words/convert-date-to-words.test.ts +++ b/src/convert-date-to-words/convert-date-to-words.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from "../_internals/test/runtime"; import { convertDateToWords, type ConvertDateToWordsOptions } from "./convert-date-to-words"; function expectDates( - cases: ReadonlyArray, + cases: readonly (readonly [string, string])[], options?: ConvertDateToWordsOptions, ): void { const mismatches = cases.filter( @@ -93,7 +93,7 @@ describe("convertDateToWords", () => { test("should ignore an invalid case value and fall back to 'lower'", () => { expect( - // @ts-expect-error + // @ts-expect-error: intentionally invalid input convertDateToWords("01/01/2024", { case: "invalid" }), ).toBe("primeiro de janeiro de dois mil e vinte e quatro"); }); @@ -111,13 +111,13 @@ describe("convertDateToWords", () => { test("should ignore an invalid style value and fall back to 'full'", () => { expect( - // @ts-expect-error + // @ts-expect-error: intentionally invalid input convertDateToWords("02/03/2024", { style: "invalid" }), ).toBe("dois de março de dois mil e vinte e quatro"); }); test("should match a hand-written string for every month in both styles", () => { - const cases: Array<[string, string, string]> = [ + const cases: [string, string, string][] = [ ["02/01/2024", "dois de janeiro de dois mil e vinte e quatro", "2 de janeiro de 2024"], ["02/02/2024", "dois de fevereiro de dois mil e vinte e quatro", "2 de fevereiro de 2024"], ["02/03/2024", "dois de março de dois mil e vinte e quatro", "2 de março de 2024"], @@ -131,13 +131,13 @@ describe("convertDateToWords", () => { ["02/11/2024", "dois de novembro de dois mil e vinte e quatro", "2 de novembro de 2024"], ["02/12/2024", "dois de dezembro de dois mil e vinte e quatro", "2 de dezembro de 2024"], ]; - const failures: Array<{ + const failures: { input: string; actualFull: string; expectedFull: string; actualMonth: string; expectedMonth: string; - }> = []; + }[] = []; for (const [input, expectedFull, expectedMonth] of cases) { const actualFull = convertDateToWords(input, { style: "full" }); @@ -169,7 +169,7 @@ describe("convertDateToWords", () => { }); test("should prefix the pt-BR weekday and a comma for 7 consecutive known dates", () => { - const cases: Array<[string, string]> = [ + const cases: [string, string][] = [ ["03/03/2024", "domingo, três de março de dois mil e vinte e quatro"], ["04/03/2024", "segunda-feira, quatro de março de dois mil e vinte e quatro"], ["05/03/2024", "terça-feira, cinco de março de dois mil e vinte e quatro"], @@ -217,18 +217,18 @@ describe("convertDateToWords", () => { }); test("should return '' for a non-Date/non-string value", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertDateToWords(null)).toBe(""); - // @ts-expect-error - expect(convertDateToWords(undefined)).toBe(""); - // @ts-expect-error - expect(convertDateToWords(20240101)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(convertDateToWords()).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(convertDateToWords(20_240_101)).toBe(""); }); test("should return '' for a non-Date/non-string value even when it stringifies to a valid date", () => { const trojan = { toString: () => "01/01/2024" }; - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertDateToWords(trojan)).toBe(""); }); @@ -305,7 +305,7 @@ describe("convertDateToWords", () => { describe("literal case tables", () => { test("should match a hand-written string for the 1st and the 15th of every month", () => { - const cases: Array<[string, string]> = [ + const cases: [string, string][] = [ ["01/01/2024", "primeiro de janeiro de dois mil e vinte e quatro"], ["15/01/2024", "quinze de janeiro de dois mil e vinte e quatro"], ["01/02/2024", "primeiro de fevereiro de dois mil e vinte e quatro"], @@ -335,7 +335,7 @@ describe("convertDateToWords", () => { }); test("should match a hand-written string for every day of a 31 day month", () => { - const cases: Array<[string, string]> = [ + const cases: [string, string][] = [ ["01/03/2024", "primeiro de março de dois mil e vinte e quatro"], ["02/03/2024", "dois de março de dois mil e vinte e quatro"], ["03/03/2024", "três de março de dois mil e vinte e quatro"], @@ -372,7 +372,7 @@ describe("convertDateToWords", () => { }); test("should reproduce every published brutils 'convert_date_to_text' example (tests/test_date_utils.py, lowercase here because brutils always capitalizes and this library exposes that as case: 'sentence')", () => { - const cases: Array<[string, string]> = [ + const cases: [string, string][] = [ ["15/08/2024", "quinze de agosto de dois mil e vinte e quatro"], ["01/01/2000", "primeiro de janeiro de dois mil"], ["31/12/1999", "trinta e um de dezembro de mil novecentos e noventa e nove"], @@ -383,7 +383,7 @@ describe("convertDateToWords", () => { }); test("should match a hand-written string for day 31 and for the leap day", () => { - const cases: Array<[string, string]> = [ + const cases: [string, string][] = [ ["31/01/2024", "trinta e um de janeiro de dois mil e vinte e quatro"], ["29/02/2024", "vinte e nove de fevereiro de dois mil e vinte e quatro"], ]; @@ -391,7 +391,7 @@ describe("convertDateToWords", () => { }); test("should render the year without the thousands comma for 1900, 1999, 2000, 2001, 2024 and 2100", () => { - const cases: Array<[string, string]> = [ + const cases: [string, string][] = [ ["01/01/1101", "primeiro de janeiro de mil cento e um"], ["01/01/1200", "primeiro de janeiro de mil e duzentos"], ["01/01/1500", "primeiro de janeiro de mil e quinhentos"], @@ -407,7 +407,7 @@ describe("convertDateToWords", () => { }); test("should give the same hand-written result for the 'dd/mm/yyyy' and the ISO form", () => { - const cases: Array<[string, string]> = [ + const cases: [string, string][] = [ ["2024-01-02", "dois de janeiro de dois mil e vinte e quatro"], ["1999-05-10", "dez de maio de mil novecentos e noventa e nove"], ]; diff --git a/src/convert-date-to-words/convert-date-to-words.ts b/src/convert-date-to-words/convert-date-to-words.ts index 97193edf..571e5523 100644 --- a/src/convert-date-to-words/convert-date-to-words.ts +++ b/src/convert-date-to-words/convert-date-to-words.ts @@ -114,9 +114,10 @@ export const convertDateToWords = ( ? `${day === 1 ? "1º" : day} de ${monthName} de ${year}` : `${day === 1 ? "primeiro" : numberToWords(day)} de ${monthName} de ${numberToWords(year).replaceAll(", ", " ")}`; - const result = options?.weekday - ? `${WEEKDAY_NAMES[getWeekdayIndex(year, month, day)]}, ${dateWords}` - : dateWords; + const result = + options?.weekday === true + ? `${WEEKDAY_NAMES[getWeekdayIndex(year, month, day)]}, ${dateWords}` + : dateWords; return applyWordsCase(result, options?.case); }; diff --git a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts index f3b5fcc9..948f116d 100644 --- a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts +++ b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts @@ -4,13 +4,13 @@ import { convertLicensePlateToMercosul } from "./convert-license-plate-to-mercos describe("convertLicensePlateToMercosul", () => { describe("should return an empty string", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertLicensePlateToMercosul(null)).toBe(""); }); test("when it is undefined", () => { - // @ts-expect-error - expect(convertLicensePlateToMercosul(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(convertLicensePlateToMercosul()).toBe(""); }); test("when it is an empty string", () => { diff --git a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts index 4d654a3f..9f418a5e 100644 --- a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts +++ b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts @@ -27,7 +27,7 @@ export const convertLicensePlateToMercosul = (value: string): string => { const parsed = parseLicensePlate(value); - const letter = DIGIT_TO_MERCOSUL_LETTER[parsed[4]]; + const letter = DIGIT_TO_MERCOSUL_LETTER[parsed.charAt(4)]; return `${parsed.slice(0, 4)}${letter}${parsed.slice(5)}`; }; diff --git a/src/convert-number-to-words/convert-number-to-words.test.ts b/src/convert-number-to-words/convert-number-to-words.test.ts index 8f59436c..0be44ed6 100644 --- a/src/convert-number-to-words/convert-number-to-words.test.ts +++ b/src/convert-number-to-words/convert-number-to-words.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from "../_internals/test/runtime"; import { convertNumberToWords, type ConvertNumberToWordsOptions } from "./convert-number-to-words"; function expectWords( - cases: ReadonlyArray, + cases: readonly (readonly [number, string])[], options?: ConvertNumberToWordsOptions, ): void { const failures = cases.filter( @@ -66,12 +66,12 @@ describe("convertNumberToWords", () => { }); test("should return '' for a non-number value", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertNumberToWords("123")).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertNumberToWords(null)).toBe(""); - // @ts-expect-error - expect(convertNumberToWords(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(convertNumberToWords()).toBe(""); }); }); @@ -103,14 +103,14 @@ describe("convertNumberToWords", () => { }); test("should ignore an invalid case value and fall back to 'lower'", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(convertNumberToWords(123, { case: "invalid" })).toBe("cento e vinte e três"); }); }); describe("literal case tables", () => { test("should match a hand-written word for every integer from 31 to 200 (masculine)", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [31, "trinta e um"], [32, "trinta e dois"], [33, "trinta e três"], @@ -286,7 +286,7 @@ describe("convertNumberToWords", () => { }); test("should match a hand-written word for every round hundred and the hundred that follows it", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [100, "cem"], [101, "cento e um"], [200, "duzentos"], @@ -311,7 +311,7 @@ describe("convertNumberToWords", () => { }); test("should match a hand-written word at every ten/hundred/thousand/scale boundary", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [999, "novecentos e noventa e nove"], [1000, "mil"], [1001, "mil e um"], @@ -325,39 +325,39 @@ describe("convertNumberToWords", () => { [2001, "dois mil e um"], [5000, "cinco mil"], [9999, "nove mil, novecentos e noventa e nove"], - [10000, "dez mil"], - [21000, "vinte e um mil"], - [100000, "cem mil"], - [101000, "cento e um mil"], - [200000, "duzentos mil"], - [300000, "trezentos mil"], - [999999, "novecentos e noventa e nove mil, novecentos e noventa e nove"], - [1000000, "um milhão"], - [1000001, "um milhão e um"], - [1000100, "um milhão e cem"], - [1000230, "um milhão, duzentos e trinta"], - [1045678, "um milhão, quarenta e cinco mil, seiscentos e setenta e oito"], - [1100000, "um milhão e cem mil"], - [1200000, "um milhão e duzentos mil"], - [1230000, "um milhão, duzentos e trinta mil"], - [1230045, "um milhão, duzentos e trinta mil e quarenta e cinco"], - [1230456, "um milhão, duzentos e trinta mil, quatrocentos e cinquenta e seis"], - [2000000, "dois milhões"], - [1000000000, "um bilhão"], - [1000000001, "um bilhão e um"], - [2000000000, "dois bilhões"], + [10_000, "dez mil"], + [21_000, "vinte e um mil"], + [100_000, "cem mil"], + [101_000, "cento e um mil"], + [200_000, "duzentos mil"], + [300_000, "trezentos mil"], + [999_999, "novecentos e noventa e nove mil, novecentos e noventa e nove"], + [1_000_000, "um milhão"], + [1_000_001, "um milhão e um"], + [1_000_100, "um milhão e cem"], + [1_000_230, "um milhão, duzentos e trinta"], + [1_045_678, "um milhão, quarenta e cinco mil, seiscentos e setenta e oito"], + [1_100_000, "um milhão e cem mil"], + [1_200_000, "um milhão e duzentos mil"], + [1_230_000, "um milhão, duzentos e trinta mil"], + [1_230_045, "um milhão, duzentos e trinta mil e quarenta e cinco"], + [1_230_456, "um milhão, duzentos e trinta mil, quatrocentos e cinquenta e seis"], + [2_000_000, "dois milhões"], + [1_000_000_000, "um bilhão"], + [1_000_000_001, "um bilhão e um"], + [2_000_000_000, "dois bilhões"], [ - 1234567890, + 1_234_567_890, "um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa", ], [ - 999999999999, + 999_999_999_999, "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", ], - [1000000000000, "um trilhão"], - [2000000000000, "dois trilhões"], + [1_000_000_000_000, "um trilhão"], + [2_000_000_000_000, "dois trilhões"], [ - 999999999999999, + 999_999_999_999_999, "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", ], ]; @@ -365,7 +365,7 @@ describe("convertNumberToWords", () => { }); test("should prefix 'menos' to a hand-written word for every integer from -1 to -100", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [-1, "menos um"], [-2, "menos dois"], [-3, "menos três"], @@ -471,15 +471,15 @@ describe("convertNumberToWords", () => { }); test("should prefix 'menos' to a hand-written word at negative scale boundaries", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [-200, "menos duzentos"], [-999, "menos novecentos e noventa e nove"], [-1000, "menos mil"], [-1001, "menos mil e um"], [-2000, "menos dois mil"], - [-1000000, "menos um milhão"], + [-1_000_000, "menos um milhão"], [ - -999999999999999, + -999_999_999_999_999, "menos novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", ], ]; @@ -487,7 +487,7 @@ describe("convertNumberToWords", () => { }); test("should match a hand-written feminine word for every integer from 0 to 30", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [0, "zero"], [1, "uma"], [2, "duas"], @@ -525,7 +525,7 @@ describe("convertNumberToWords", () => { }); test("should match a hand-written feminine word at hundred/thousand/million boundaries", () => { - const cases: Array<[number, string]> = [ + const cases: [number, string][] = [ [100, "cem"], [101, "cento e uma"], [200, "duzentas"], @@ -544,14 +544,14 @@ describe("convertNumberToWords", () => { [2000, "duas mil"], [2002, "duas mil e duas"], [3000, "três mil"], - [21000, "vinte e uma mil"], - [100000, "cem mil"], - [200000, "duzentas mil"], - [300000, "trezentas mil"], - [1000000, "um milhão"], - [1000001, "um milhão e uma"], - [2000000, "dois milhões"], - [2000002, "dois milhões e duas"], + [21_000, "vinte e uma mil"], + [100_000, "cem mil"], + [200_000, "duzentas mil"], + [300_000, "trezentas mil"], + [1_000_000, "um milhão"], + [1_000_001, "um milhão e uma"], + [2_000_000, "dois milhões"], + [2_000_002, "dois milhões e duas"], ]; expectWords(cases, { gender: "feminine" }); diff --git a/src/difference-in-business-days/difference-in-business-days.test.ts b/src/difference-in-business-days/difference-in-business-days.test.ts index aa4acd2c..bc19e5e7 100644 --- a/src/difference-in-business-days/difference-in-business-days.test.ts +++ b/src/difference-in-business-days/difference-in-business-days.test.ts @@ -146,22 +146,22 @@ describe("differenceInBusinessDays", () => { describe("invalid input", () => { it("should return null when params is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(differenceInBusinessDays(null)).toBeNull(); }); it("should return null when params is undefined", () => { - // @ts-expect-error - expect(differenceInBusinessDays(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(differenceInBusinessDays()).toBeNull(); }); it("should return null when params is not an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(differenceInBusinessDays("2024-01-02")).toBeNull(); }); it('should return null when params is a function, even one carrying from/to properties (typeof params !== "object" must reject it, not just isNullish)', () => { - const fakeParams = Object.assign(() => {}, { + const fakeParams = Object.assign(() => null, { from: new Date(2024, 0, 2), to: new Date(2024, 0, 3), }); @@ -183,14 +183,14 @@ describe("differenceInBusinessDays", () => { it("should return null when from is not a Date", () => { expect( - // @ts-expect-error + // @ts-expect-error: intentionally invalid input differenceInBusinessDays({ from: "2024-01-02", to: new Date(2024, 0, 3) }), ).toBeNull(); }); it("should return null when to is not a Date", () => { expect( - // @ts-expect-error + // @ts-expect-error: intentionally invalid input differenceInBusinessDays({ from: new Date(2024, 0, 2), to: "2024-01-03" }), ).toBeNull(); }); @@ -200,7 +200,7 @@ describe("differenceInBusinessDays", () => { differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 3), - // @ts-expect-error + // @ts-expect-error: intentionally invalid input stateCode: 11, }), ).toBeNull(); @@ -210,7 +210,7 @@ describe("differenceInBusinessDays", () => { const result = differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 3), - // @ts-expect-error + // @ts-expect-error: intentionally invalid input stateCode: "XX", }); diff --git a/src/format-boleto/format-boleto.test.ts b/src/format-boleto/format-boleto.test.ts index 5861179b..c0e4c902 100644 --- a/src/format-boleto/format-boleto.test.ts +++ b/src/format-boleto/format-boleto.test.ts @@ -119,10 +119,10 @@ describe("formatBoleto", () => { }); test("should return an empty string when the value is nullish", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatBoleto(null)).toBe(""); - // @ts-expect-error - expect(formatBoleto(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatBoleto()).toBe(""); }); describe("arrecadação", () => { diff --git a/src/format-caepf/format-caepf.test.ts b/src/format-caepf/format-caepf.test.ts index 9d788a5d..fe440b72 100644 --- a/src/format-caepf/format-caepf.test.ts +++ b/src/format-caepf/format-caepf.test.ts @@ -7,7 +7,7 @@ describe("formatCaepf", () => { }); test("should format a number input", () => { - expect(formatCaepf(41142260000101)).toBe("411.422.600/001-01"); + expect(formatCaepf(41_142_260_000_101)).toBe("411.422.600/001-01"); }); test("should format progressively as digits are typed", () => { @@ -40,12 +40,12 @@ describe("formatCaepf", () => { }); test("should return an empty string for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCaepf(null)).toBe(""); }); test("should return an empty string for undefined", () => { - // @ts-expect-error - expect(formatCaepf(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCaepf()).toBe(""); }); }); diff --git a/src/format-cei/format-cei.test.ts b/src/format-cei/format-cei.test.ts index 7ae8d28e..87f2b8ef 100644 --- a/src/format-cei/format-cei.test.ts +++ b/src/format-cei/format-cei.test.ts @@ -7,7 +7,7 @@ describe("formatCei", () => { }); test("should format a number input", () => { - expect(formatCei(249859674386)).toBe("24.985.96743/86"); + expect(formatCei(249_859_674_386)).toBe("24.985.96743/86"); }); test("should format progressively as digits are typed", () => { @@ -36,12 +36,12 @@ describe("formatCei", () => { }); test("should return an empty string for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCei(null)).toBe(""); }); test("should return an empty string for undefined", () => { - // @ts-expect-error - expect(formatCei(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCei()).toBe(""); }); }); diff --git a/src/format-cep/format-cep.test.ts b/src/format-cep/format-cep.test.ts index 148685d5..331cec21 100644 --- a/src/format-cep/format-cep.test.ts +++ b/src/format-cep/format-cep.test.ts @@ -24,9 +24,9 @@ describe("formatCep", () => { }); it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCep(null)).toBe(""); - // @ts-expect-error - expect(formatCep(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCep()).toBe(""); }); }); diff --git a/src/format-certidao/format-certidao.test.ts b/src/format-certidao/format-certidao.test.ts index 7185a9f8..4749b3ef 100644 --- a/src/format-certidao/format-certidao.test.ts +++ b/src/format-certidao/format-certidao.test.ts @@ -4,13 +4,13 @@ import { formatCertidao } from "./format-certidao"; describe("formatCertidao", () => { describe("should return an empty string", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCertidao(null)).toBe(""); }); test("when it is undefined", () => { - // @ts-expect-error - expect(formatCertidao(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCertidao()).toBe(""); }); test("when it is an empty string", () => { @@ -60,7 +60,7 @@ describe("formatCertidao", () => { describe("should accept a number", () => { test("for a value short enough to be an exact integer", () => { - expect(formatCertidao(104539015520)).toBe("104539 01 55 20"); + expect(formatCertidao(104_539_015_520)).toBe("104539 01 55 20"); }); }); }); diff --git a/src/format-cnae/format-cnae.test.ts b/src/format-cnae/format-cnae.test.ts index 0432cadc..2d444c67 100644 --- a/src/format-cnae/format-cnae.test.ts +++ b/src/format-cnae/format-cnae.test.ts @@ -7,7 +7,7 @@ describe("formatCnae", () => { }); it("should format a CNAE code given as a number", () => { - expect(formatCnae(6201501)).toBe("6201-5/01"); + expect(formatCnae(6_201_501)).toBe("6201-5/01"); }); it("should format a CNAE code that already has the mask", () => { @@ -31,6 +31,6 @@ describe("formatCnae", () => { // @ts-expect-error not a string or number expect(formatCnae(null)).toBe(""); // @ts-expect-error not a string or number - expect(formatCnae(undefined)).toBe(""); + expect(formatCnae()).toBe(""); }); }); diff --git a/src/format-cnh/format-cnh.test.ts b/src/format-cnh/format-cnh.test.ts index abcd258a..28607917 100644 --- a/src/format-cnh/format-cnh.test.ts +++ b/src/format-cnh/format-cnh.test.ts @@ -22,9 +22,9 @@ describe("formatCnh", () => { }); it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCnh(null)).toBe(""); - // @ts-expect-error - expect(formatCnh(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCnh()).toBe(""); }); }); diff --git a/src/format-cno/format-cno.test.ts b/src/format-cno/format-cno.test.ts index a701adb3..7972e798 100644 --- a/src/format-cno/format-cno.test.ts +++ b/src/format-cno/format-cno.test.ts @@ -7,7 +7,7 @@ describe("formatCno", () => { }); test("should format a number input", () => { - expect(formatCno(401800097960)).toBe("40.180.00979/60"); + expect(formatCno(401_800_097_960)).toBe("40.180.00979/60"); }); test("should format progressively as digits are typed", () => { @@ -36,12 +36,12 @@ describe("formatCno", () => { }); test("should return an empty string for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCno(null)).toBe(""); }); test("should return an empty string for undefined", () => { - // @ts-expect-error - expect(formatCno(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCno()).toBe(""); }); }); diff --git a/src/format-cnpj/format-cnpj.test.ts b/src/format-cnpj/format-cnpj.test.ts index 72f0e902..5fcbcd7b 100644 --- a/src/format-cnpj/format-cnpj.test.ts +++ b/src/format-cnpj/format-cnpj.test.ts @@ -4,10 +4,10 @@ import { formatCnpj } from "./format-cnpj"; describe("formatCnpj", () => { it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCnpj(null)).toBe(""); - // @ts-expect-error - expect(formatCnpj(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCnpj()).toBe(""); }); it("should format cnpj with mask", () => { @@ -33,16 +33,16 @@ describe("formatCnpj", () => { expect(formatCnpj(46)).toBe("46"); expect(formatCnpj(468)).toBe("46.8"); expect(formatCnpj(4684)).toBe("46.84"); - expect(formatCnpj(46843)).toBe("46.843"); - expect(formatCnpj(468434)).toBe("46.843.4"); - expect(formatCnpj(4684348)).toBe("46.843.48"); - expect(formatCnpj(46843485)).toBe("46.843.485"); - expect(formatCnpj(468434850)).toBe("46.843.485/0"); - expect(formatCnpj(4684348500)).toBe("46.843.485/00"); - expect(formatCnpj(46843485000)).toBe("46.843.485/000"); - expect(formatCnpj(468434850001)).toBe("46.843.485/0001"); - expect(formatCnpj(4684348500018)).toBe("46.843.485/0001-8"); - expect(formatCnpj(46843485000186)).toBe("46.843.485/0001-86"); + expect(formatCnpj(46_843)).toBe("46.843"); + expect(formatCnpj(468_434)).toBe("46.843.4"); + expect(formatCnpj(4_684_348)).toBe("46.843.48"); + expect(formatCnpj(46_843_485)).toBe("46.843.485"); + expect(formatCnpj(468_434_850)).toBe("46.843.485/0"); + expect(formatCnpj(4_684_348_500)).toBe("46.843.485/00"); + expect(formatCnpj(46_843_485_000)).toBe("46.843.485/000"); + expect(formatCnpj(468_434_850_001)).toBe("46.843.485/0001"); + expect(formatCnpj(4_684_348_500_018)).toBe("46.843.485/0001-8"); + expect(formatCnpj(46_843_485_000_186)).toBe("46.843.485/0001-86"); }); it("should format cnpj with mask filling zeroes", () => { @@ -68,16 +68,16 @@ describe("formatCnpj", () => { expect(formatCnpj(46, { pad: true })).toBe("00.000.000/0000-46"); expect(formatCnpj(468, { pad: true })).toBe("00.000.000/0004-68"); expect(formatCnpj(4684, { pad: true })).toBe("00.000.000/0046-84"); - expect(formatCnpj(46843, { pad: true })).toBe("00.000.000/0468-43"); - expect(formatCnpj(468434, { pad: true })).toBe("00.000.000/4684-34"); - expect(formatCnpj(4684348, { pad: true })).toBe("00.000.004/6843-48"); - expect(formatCnpj(46843485, { pad: true })).toBe("00.000.046/8434-85"); - expect(formatCnpj(468434850, { pad: true })).toBe("00.000.468/4348-50"); - expect(formatCnpj(4684348500, { pad: true })).toBe("00.004.684/3485-00"); - expect(formatCnpj(46843485000, { pad: true })).toBe("00.046.843/4850-00"); - expect(formatCnpj(468434850001, { pad: true })).toBe("00.468.434/8500-01"); - expect(formatCnpj(4684348500018, { pad: true })).toBe("04.684.348/5000-18"); - expect(formatCnpj(46843485000186, { pad: true })).toBe("46.843.485/0001-86"); + expect(formatCnpj(46_843, { pad: true })).toBe("00.000.000/0468-43"); + expect(formatCnpj(468_434, { pad: true })).toBe("00.000.000/4684-34"); + expect(formatCnpj(4_684_348, { pad: true })).toBe("00.000.004/6843-48"); + expect(formatCnpj(46_843_485, { pad: true })).toBe("00.000.046/8434-85"); + expect(formatCnpj(468_434_850, { pad: true })).toBe("00.000.468/4348-50"); + expect(formatCnpj(4_684_348_500, { pad: true })).toBe("00.004.684/3485-00"); + expect(formatCnpj(46_843_485_000, { pad: true })).toBe("00.046.843/4850-00"); + expect(formatCnpj(468_434_850_001, { pad: true })).toBe("00.468.434/8500-01"); + expect(formatCnpj(4_684_348_500_018, { pad: true })).toBe("04.684.348/5000-18"); + expect(formatCnpj(46_843_485_000_186, { pad: true })).toBe("46.843.485/0001-86"); }); it(`should NOT add digits after the CNPJ length (${CNPJ_LENGTH})`, () => { @@ -118,7 +118,7 @@ describe("formatCnpj", () => { it("should hide the first 2 digits and the 2 check digits when obfuscate is true", () => { expect(formatCnpj("46843485000186", { obfuscate: true })).toBe("**.843.485/0001-**"); - expect(formatCnpj(46843485000186, { obfuscate: true })).toBe("**.843.485/0001-**"); + expect(formatCnpj(46_843_485_000_186, { obfuscate: true })).toBe("**.843.485/0001-**"); }); it("should pad before obfuscating", () => { diff --git a/src/format-cnpj/format-cnpj.ts b/src/format-cnpj/format-cnpj.ts index a1b5422a..5661bfb5 100644 --- a/src/format-cnpj/format-cnpj.ts +++ b/src/format-cnpj/format-cnpj.ts @@ -11,7 +11,7 @@ export type FormatCnpjOptions = Pick & { obfuscate?: boolean; }; -const sanitize = (value: string | number, version?: FormatCnpjOptions["version"]) => { +const sanitize = (value: string | number, version?: FormatCnpjOptions["version"]): string => { if (version === 2) { return sanitizeToAlphanumeric(value); } @@ -48,6 +48,6 @@ export const formatCnpj = (value: string | number, options?: FormatCnpjOptions): return format({ pad: options?.pad, value: sanitize(value, options?.version), - pattern: options?.obfuscate ? OBFUSCATED_PATTERN : PATTERN, + pattern: options?.obfuscate === true ? OBFUSCATED_PATTERN : PATTERN, }); }; diff --git a/src/format-cns/format-cns.test.ts b/src/format-cns/format-cns.test.ts index f155599b..1c93168d 100644 --- a/src/format-cns/format-cns.test.ts +++ b/src/format-cns/format-cns.test.ts @@ -12,7 +12,7 @@ describe("formatCns", () => { }); it("should format a number CNS with the space mask", () => { - expect(formatCns(123456789010001)).toBe("123 4567 8901 0001"); + expect(formatCns(123_456_789_010_001)).toBe("123 4567 8901 0001"); }); it("should pad the value with leading zeros when pad is true", () => { @@ -29,9 +29,9 @@ describe("formatCns", () => { }); it("should return an empty string when the value is null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCns(null)).toBe(""); - // @ts-expect-error - expect(formatCns(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCns()).toBe(""); }); }); diff --git a/src/format-cpf/format-cpf.test.ts b/src/format-cpf/format-cpf.test.ts index da6eb160..7d40adce 100644 --- a/src/format-cpf/format-cpf.test.ts +++ b/src/format-cpf/format-cpf.test.ts @@ -4,10 +4,10 @@ import { formatCpf } from "./format-cpf"; describe("formatCpf", () => { it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatCpf(null)).toBe(""); - // @ts-expect-error - expect(formatCpf(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCpf()).toBe(""); }); it("should format CPF with mask", () => { @@ -30,13 +30,13 @@ describe("formatCpf", () => { expect(formatCpf(94)).toBe("94"); expect(formatCpf(943)).toBe("943"); expect(formatCpf(9438)).toBe("943.8"); - expect(formatCpf(94389)).toBe("943.89"); - expect(formatCpf(943895)).toBe("943.895"); - expect(formatCpf(9438957)).toBe("943.895.7"); - expect(formatCpf(94389575)).toBe("943.895.75"); - expect(formatCpf(943895751)).toBe("943.895.751"); - expect(formatCpf(9438957510)).toBe("943.895.751-0"); - expect(formatCpf(94389575104)).toBe("943.895.751-04"); + expect(formatCpf(94_389)).toBe("943.89"); + expect(formatCpf(943_895)).toBe("943.895"); + expect(formatCpf(9_438_957)).toBe("943.895.7"); + expect(formatCpf(94_389_575)).toBe("943.895.75"); + expect(formatCpf(943_895_751)).toBe("943.895.751"); + expect(formatCpf(9_438_957_510)).toBe("943.895.751-0"); + expect(formatCpf(94_389_575_104)).toBe("943.895.751-04"); }); it("should format CPF with mask filling zeroes", () => { @@ -59,13 +59,13 @@ describe("formatCpf", () => { expect(formatCpf(94, { pad: true })).toBe("000.000.000-94"); expect(formatCpf(943, { pad: true })).toBe("000.000.009-43"); expect(formatCpf(9438, { pad: true })).toBe("000.000.094-38"); - expect(formatCpf(94389, { pad: true })).toBe("000.000.943-89"); - expect(formatCpf(943895, { pad: true })).toBe("000.009.438-95"); - expect(formatCpf(9438957, { pad: true })).toBe("000.094.389-57"); - expect(formatCpf(94389575, { pad: true })).toBe("000.943.895-75"); - expect(formatCpf(943895751, { pad: true })).toBe("009.438.957-51"); - expect(formatCpf(9438957510, { pad: true })).toBe("094.389.575-10"); - expect(formatCpf(94389575104, { pad: true })).toBe("943.895.751-04"); + expect(formatCpf(94_389, { pad: true })).toBe("000.000.943-89"); + expect(formatCpf(943_895, { pad: true })).toBe("000.009.438-95"); + expect(formatCpf(9_438_957, { pad: true })).toBe("000.094.389-57"); + expect(formatCpf(94_389_575, { pad: true })).toBe("000.943.895-75"); + expect(formatCpf(943_895_751, { pad: true })).toBe("009.438.957-51"); + expect(formatCpf(9_438_957_510, { pad: true })).toBe("094.389.575-10"); + expect(formatCpf(94_389_575_104, { pad: true })).toBe("943.895.751-04"); }); it(`should NOT add digits after the CPF length (${CPF_LENGTH})`, () => { @@ -78,7 +78,7 @@ describe("formatCpf", () => { it("should hide the first 3 digits and the 2 check digits when obfuscate is true", () => { expect(formatCpf("94389575104", { obfuscate: true })).toBe("***.895.751-**"); - expect(formatCpf(94389575104, { obfuscate: true })).toBe("***.895.751-**"); + expect(formatCpf(94_389_575_104, { obfuscate: true })).toBe("***.895.751-**"); }); it("should pad before obfuscating", () => { diff --git a/src/format-cpf/format-cpf.ts b/src/format-cpf/format-cpf.ts index e6f6d6be..300bd143 100644 --- a/src/format-cpf/format-cpf.ts +++ b/src/format-cpf/format-cpf.ts @@ -33,6 +33,6 @@ export const formatCpf = (value: string | number, options?: FormatCpfOptions): s return format({ pad: options?.pad, value: sanitizeToDigits(value), - pattern: options?.obfuscate ? OBFUSCATED_PATTERN : PATTERN, + pattern: options?.obfuscate === true ? OBFUSCATED_PATTERN : PATTERN, }); }; diff --git a/src/format-currency/format-currency.test.ts b/src/format-currency/format-currency.test.ts index c9729a9e..1a91d24d 100644 --- a/src/format-currency/format-currency.test.ts +++ b/src/format-currency/format-currency.test.ts @@ -11,9 +11,9 @@ describe("formatCurrency", () => { expect(formatCurrency(10.01)).toBe("10,01"); expect(formatCurrency(100.01)).toBe("100,01"); expect(formatCurrency(1000.01)).toBe("1.000,01"); - expect(formatCurrency(10000.01)).toBe("10.000,01"); - expect(formatCurrency(100000.01)).toBe("100.000,01"); - expect(formatCurrency(1000000.01)).toBe("1.000.000,01"); + expect(formatCurrency(10_000.01)).toBe("10.000,01"); + expect(formatCurrency(100_000.01)).toBe("100.000,01"); + expect(formatCurrency(1_000_000.01)).toBe("1.000.000,01"); }); it("should formatCurrency negative currency into BRL", () => { @@ -25,9 +25,9 @@ describe("formatCurrency", () => { expect(formatCurrency(-10.01)).toBe("-10,01"); expect(formatCurrency(-100.01)).toBe("-100,01"); expect(formatCurrency(-1000.01)).toBe("-1.000,01"); - expect(formatCurrency(-10000.01)).toBe("-10.000,01"); - expect(formatCurrency(-100000.01)).toBe("-100.000,01"); - expect(formatCurrency(-1000000.01)).toBe("-1.000.000,01"); + expect(formatCurrency(-10_000.01)).toBe("-10.000,01"); + expect(formatCurrency(-100_000.01)).toBe("-100.000,01"); + expect(formatCurrency(-1_000_000.01)).toBe("-1.000.000,01"); }); it("should formatCurrency positive currency into BRL with currency sign", () => { @@ -39,9 +39,9 @@ describe("formatCurrency", () => { expect(formatCurrency(10.01, { symbol: true })).toBe("R$ 10,01"); expect(formatCurrency(100.01, { symbol: true })).toBe("R$ 100,01"); expect(formatCurrency(1000.01, { symbol: true })).toBe("R$ 1.000,01"); - expect(formatCurrency(10000.01, { symbol: true })).toBe("R$ 10.000,01"); - expect(formatCurrency(100000.01, { symbol: true })).toBe("R$ 100.000,01"); - expect(formatCurrency(1000000.01, { symbol: true })).toBe("R$ 1.000.000,01"); + expect(formatCurrency(10_000.01, { symbol: true })).toBe("R$ 10.000,01"); + expect(formatCurrency(100_000.01, { symbol: true })).toBe("R$ 100.000,01"); + expect(formatCurrency(1_000_000.01, { symbol: true })).toBe("R$ 1.000.000,01"); }); it("should formatCurrency negative currency into BRL with currency sign", () => { @@ -53,9 +53,9 @@ describe("formatCurrency", () => { expect(formatCurrency(-10.01, { symbol: true })).toBe("-R$ 10,01"); expect(formatCurrency(-100.01, { symbol: true })).toBe("-R$ 100,01"); expect(formatCurrency(-1000.01, { symbol: true })).toBe("-R$ 1.000,01"); - expect(formatCurrency(-10000.01, { symbol: true })).toBe("-R$ 10.000,01"); - expect(formatCurrency(-100000.01, { symbol: true })).toBe("-R$ 100.000,01"); - expect(formatCurrency(-1000000.01, { symbol: true })).toBe("-R$ 1.000.000,01"); + expect(formatCurrency(-10_000.01, { symbol: true })).toBe("-R$ 10.000,01"); + expect(formatCurrency(-100_000.01, { symbol: true })).toBe("-R$ 100.000,01"); + expect(formatCurrency(-1_000_000.01, { symbol: true })).toBe("-R$ 1.000.000,01"); }); it("should formatCurrency with different precision", () => { @@ -67,9 +67,9 @@ describe("formatCurrency", () => { expect(formatCurrency(10.001, { precision: 3 })).toBe("10,001"); expect(formatCurrency(100.001, { precision: 3 })).toBe("100,001"); expect(formatCurrency(1000.001, { precision: 3 })).toBe("1.000,001"); - expect(formatCurrency(10000.001, { precision: 3 })).toBe("10.000,001"); - expect(formatCurrency(100000.001, { precision: 3 })).toBe("100.000,001"); - expect(formatCurrency(1000000.001, { precision: 3 })).toBe("1.000.000,001"); + expect(formatCurrency(10_000.001, { precision: 3 })).toBe("10.000,001"); + expect(formatCurrency(100_000.001, { precision: 3 })).toBe("100.000,001"); + expect(formatCurrency(1_000_000.001, { precision: 3 })).toBe("1.000.000,001"); }); it("should read the separators of string inputs", () => { @@ -106,8 +106,8 @@ describe("formatCurrency", () => { expect(formatCurrency(Number.POSITIVE_INFINITY)).toBe(""); expect(formatCurrency(Number.NEGATIVE_INFINITY)).toBe(""); expect(formatCurrency(Number.NaN, { symbol: true })).toBe(""); - // @ts-expect-error - expect(formatCurrency(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCurrency()).toBe(""); }); it("should read as many fraction digits as the requested precision allows, not just the default 2, when reading a string", () => { @@ -116,6 +116,6 @@ describe("formatCurrency", () => { it("should replace the non-breaking space", () => { expect(formatCurrency(1234.56, { symbol: true })).toBe("R$ 1.234,56"); - expect(formatCurrency(1234.56, { symbol: true })).not.toContain("\u00a0"); + expect(formatCurrency(1234.56, { symbol: true })).not.toContain("\u00A0"); }); }); diff --git a/src/format-currency/format-currency.ts b/src/format-currency/format-currency.ts index 1ec6c273..14313911 100644 --- a/src/format-currency/format-currency.ts +++ b/src/format-currency/format-currency.ts @@ -78,5 +78,5 @@ export const formatCurrency = (value: string | number, options?: FormatCurrencyO return getFormatter(Boolean(options?.symbol), precision) .format(enhancedValue) - .replace("\u00a0", " "); + .replace("\u00A0", " "); }; diff --git a/src/format-iban/format-iban.test.ts b/src/format-iban/format-iban.test.ts index 8c958dfd..e5efa047 100644 --- a/src/format-iban/format-iban.test.ts +++ b/src/format-iban/format-iban.test.ts @@ -37,17 +37,17 @@ describe("formatIban", () => { }); it("should return an empty string when the value is not a string", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatIban(null)).toBe(""); - // @ts-expect-error - expect(formatIban(undefined)).toBe(""); - // @ts-expect-error - expect(formatIban(1500000000000)).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input + expect(formatIban()).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatIban(1_500_000_000_000)).toBe(""); + // @ts-expect-error: intentionally invalid input expect(formatIban(true)).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatIban({})).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatIban([])).toBe(""); }); }); diff --git a/src/format-legal-nature/format-legal-nature.test.ts b/src/format-legal-nature/format-legal-nature.test.ts index 656ff9d5..90750f3c 100644 --- a/src/format-legal-nature/format-legal-nature.test.ts +++ b/src/format-legal-nature/format-legal-nature.test.ts @@ -11,9 +11,9 @@ describe("formatLegalNature", () => { }); it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatLegalNature(null)).toBe(""); - // @ts-expect-error - expect(formatLegalNature(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatLegalNature()).toBe(""); }); }); diff --git a/src/format-ncm/format-ncm.test.ts b/src/format-ncm/format-ncm.test.ts index 6bd6aa0a..260984b2 100644 --- a/src/format-ncm/format-ncm.test.ts +++ b/src/format-ncm/format-ncm.test.ts @@ -7,7 +7,7 @@ describe("formatNcm", () => { }); it("should format an NCM code given as a number", () => { - expect(formatNcm(84713012)).toBe("8471.30.12"); + expect(formatNcm(84_713_012)).toBe("8471.30.12"); }); it("should format an NCM code that already has the mask", () => { @@ -36,6 +36,6 @@ describe("formatNcm", () => { // @ts-expect-error not a string or number expect(formatNcm(null)).toBe(""); // @ts-expect-error not a string or number - expect(formatNcm(undefined)).toBe(""); + expect(formatNcm()).toBe(""); }); }); diff --git a/src/format-nfe-key/format-nfe-key.test.ts b/src/format-nfe-key/format-nfe-key.test.ts index d320e067..7adfa848 100644 --- a/src/format-nfe-key/format-nfe-key.test.ts +++ b/src/format-nfe-key/format-nfe-key.test.ts @@ -27,20 +27,20 @@ describe("formatNfeKey", () => { }); test("should return an empty string for nullish input", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatNfeKey(null)).toBe(""); - // @ts-expect-error - expect(formatNfeKey(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatNfeKey()).toBe(""); }); test("should not throw for other bad input types", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatNfeKey(123)).toBe("123"); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatNfeKey({})).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatNfeKey([])).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatNfeKey(true)).toBe(""); }); }); diff --git a/src/format-passport/format-passport.test.ts b/src/format-passport/format-passport.test.ts index dfbf5bbd..22488337 100644 --- a/src/format-passport/format-passport.test.ts +++ b/src/format-passport/format-passport.test.ts @@ -38,11 +38,11 @@ describe("formatPassport", () => { describe("should return an empty string", () => { test("when passport is not a string", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatPassport(null)).toBe(""); - // @ts-expect-error - expect(formatPassport(undefined)).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input + expect(formatPassport()).toBe(""); + // @ts-expect-error: intentionally invalid input expect(formatPassport(123)).toBe(""); }); }); diff --git a/src/format-passport/format-passport.ts b/src/format-passport/format-passport.ts index e4c324e1..add73258 100644 --- a/src/format-passport/format-passport.ts +++ b/src/format-passport/format-passport.ts @@ -5,8 +5,8 @@ import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/s * Formats a Brazilian passport number for display. * Converts to uppercase and removes all non-alphanumeric characters. * - * @param passport - A Brazilian passport number (any case, possibly with symbols). - * @returns The formatted passport number (uppercase, no symbols), or an empty string if invalid. + * @param {string} passport - A Brazilian passport number (any case, possibly with symbols). + * @returns {string} The formatted passport number (uppercase, no symbols), or an empty string if invalid. * * @example * formatPassport("ab123456") // "AB123456" diff --git a/src/format-phone/format-phone.test.ts b/src/format-phone/format-phone.test.ts index 00e2fa1a..cc0f55f0 100644 --- a/src/format-phone/format-phone.test.ts +++ b/src/format-phone/format-phone.test.ts @@ -64,7 +64,7 @@ describe("formatPhone", () => { expect(formatPhone("+55 0800 123 4567", { mask: "e164" })).toBe("0800 123 4567"); expect(formatPhone("+55 0800 123 4567", { mask: "international" })).toBe("0800 123 4567"); expect(formatPhone("55988887777", { mask: "e164" })).toBe("+5555988887777"); - expect(formatPhone(11988887777, { mask: "e164" })).toBe("+5511988887777"); + expect(formatPhone(11_988_887_777, { mask: "e164" })).toBe("+5511988887777"); }); it("should international format phone", () => { @@ -138,13 +138,13 @@ describe("formatPhone", () => { }); it("should return an empty string for nullish values", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatPhone(null)).toBe(""); - // @ts-expect-error - expect(formatPhone(undefined)).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input + expect(formatPhone()).toBe(""); + // @ts-expect-error: intentionally invalid input expect(formatPhone(null, { mask: "e164" })).toBe(""); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatPhone(undefined, { mask: "service" })).toBe(""); }); }); diff --git a/src/format-pis/format-pis.test.ts b/src/format-pis/format-pis.test.ts index 86d8b2b2..0b24f4ff 100644 --- a/src/format-pis/format-pis.test.ts +++ b/src/format-pis/format-pis.test.ts @@ -44,15 +44,15 @@ describe("formatPis", () => { expect(formatPis(10)).toBe("10"); expect(formatPis(100)).toBe("100"); expect(formatPis(1000)).toBe("100.0"); - expect(formatPis(10000)).toBe("100.00"); - expect(formatPis(100000)).toBe("100.000"); - expect(formatPis(1000000)).toBe("100.0000"); - expect(formatPis(10000000)).toBe("100.00000"); - expect(formatPis(100000000)).toBe("100.00000.0"); - expect(formatPis(1000000000)).toBe("100.00000.00"); - expect(formatPis(10000000000)).toBe("100.00000.00-0"); - expect(formatPis(100000000000)).toBe("100.00000.00-0"); - expect(formatPis(1000000000000)).toBe("100.00000.00-0"); + expect(formatPis(10_000)).toBe("100.00"); + expect(formatPis(100_000)).toBe("100.000"); + expect(formatPis(1_000_000)).toBe("100.0000"); + expect(formatPis(10_000_000)).toBe("100.00000"); + expect(formatPis(100_000_000)).toBe("100.00000.0"); + expect(formatPis(1_000_000_000)).toBe("100.00000.00"); + expect(formatPis(10_000_000_000)).toBe("100.00000.00-0"); + expect(formatPis(100_000_000_000)).toBe("100.00000.00-0"); + expect(formatPis(1_000_000_000_000)).toBe("100.00000.00-0"); }); it("when it is a float number", () => { @@ -77,9 +77,9 @@ describe("formatPis", () => { }); it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatPis(null)).toBe(""); - // @ts-expect-error - expect(formatPis(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatPis()).toBe(""); }); }); diff --git a/src/format-processo-juridico/format-processo-juridico.test.ts b/src/format-processo-juridico/format-processo-juridico.test.ts index 104ce5da..33901ec5 100644 --- a/src/format-processo-juridico/format-processo-juridico.test.ts +++ b/src/format-processo-juridico/format-processo-juridico.test.ts @@ -42,9 +42,9 @@ describe("formatProcessoJuridico", () => { }); it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatProcessoJuridico(null)).toBe(""); - // @ts-expect-error - expect(formatProcessoJuridico(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatProcessoJuridico()).toBe(""); }); }); diff --git a/src/format-voter-id/format-voter-id.test.ts b/src/format-voter-id/format-voter-id.test.ts index 33673743..a3d78826 100644 --- a/src/format-voter-id/format-voter-id.test.ts +++ b/src/format-voter-id/format-voter-id.test.ts @@ -33,9 +33,9 @@ describe("formatVoterId", () => { }); it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(formatVoterId(null)).toBe(""); - // @ts-expect-error - expect(formatVoterId(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatVoterId()).toBe(""); }); }); diff --git a/src/generate-boleto/generate-boleto.ts b/src/generate-boleto/generate-boleto.ts index 34ccd21d..5ed8ef7e 100644 --- a/src/generate-boleto/generate-boleto.ts +++ b/src/generate-boleto/generate-boleto.ts @@ -38,7 +38,9 @@ const generateBancario = (): string => { const generateArrecadacao = (): string => { const segment = ARRECADACAO_SEGMENTS[Math.floor(Math.random() * ARRECADACAO_SEGMENTS.length)]; const useMod11 = Math.random() < 0.5; - const checkDigit = useMod11 ? (value: string) => mod11(value, { variant: "arrecadacao" }) : mod10; + const checkDigit = useMod11 + ? (value: string): number => mod11(value, { variant: "arrecadacao" }) + : mod10; const body = generateRandomNumber(40); const head = `${ARRECADACAO_PRODUCT}${segment}${useMod11 ? "8" : "6"}`; diff --git a/src/generate-cnpj/generate-cnpj.ts b/src/generate-cnpj/generate-cnpj.ts index 087c4732..99abff57 100644 --- a/src/generate-cnpj/generate-cnpj.ts +++ b/src/generate-cnpj/generate-cnpj.ts @@ -8,7 +8,7 @@ const BASE_LENGTH = 12; const VALID_CNPJ_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const generateRandomCnpjChar = (): string => - VALID_CNPJ_CHARS[Math.floor(Math.random() * VALID_CNPJ_CHARS.length)]; + VALID_CNPJ_CHARS.charAt(Math.floor(Math.random() * VALID_CNPJ_CHARS.length)); const generateAlphanumericCnpjBase = (): string => { let base = ""; @@ -28,13 +28,8 @@ const generateNonRepeatedBase = (generate: () => string): string => { const charToCnpjValue = (char: string): number => char.charCodeAt(0) - 48; -const generateAlphanumericChecksum = (cnpj: string, weights: number[]): number => { - let sum = 0; - for (let i = 0; i < cnpj.length; i++) { - sum += charToCnpjValue(cnpj[i]) * weights[i]; - } - return sum; -}; +const generateAlphanumericChecksum = (cnpj: string, weights: number[]): number => + weights.reduce((sum, weight, index) => sum + charToCnpjValue(cnpj.charAt(index)) * weight, 0); const calculateCheckDigit = (base: string, weights: number[]): string => { const mod = generateChecksum({ base, weight: weights }) % 11; diff --git a/src/generate-cpf/generate-cpf.test.ts b/src/generate-cpf/generate-cpf.test.ts index 0eba1b49..08b13ae6 100644 --- a/src/generate-cpf/generate-cpf.test.ts +++ b/src/generate-cpf/generate-cpf.test.ts @@ -50,7 +50,7 @@ describe("generateCpf", () => { }); test("should fall back to a random digit instead of looking up an unknown state code", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input const cpf = generateCpf("XX"); expect(cpf).toHaveLength(CPF_LENGTH); expect(isValidCpf(cpf)).toBe(true); diff --git a/src/generate-legal-nature/generate-legal-nature.ts b/src/generate-legal-nature/generate-legal-nature.ts index 920122a2..1c380229 100644 --- a/src/generate-legal-nature/generate-legal-nature.ts +++ b/src/generate-legal-nature/generate-legal-nature.ts @@ -16,5 +16,6 @@ import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; */ export const generateLegalNature = (): string => { const legalNatureCodes = Object.keys(LEGAL_NATURE); + return legalNatureCodes[Math.floor(Math.random() * legalNatureCodes.length)]; }; diff --git a/src/generate-license-plate/generate-license-plate.test.ts b/src/generate-license-plate/generate-license-plate.test.ts index ea5e3f57..9dde61ca 100644 --- a/src/generate-license-plate/generate-license-plate.test.ts +++ b/src/generate-license-plate/generate-license-plate.test.ts @@ -34,7 +34,7 @@ describe("generateLicensePlate", () => { }); it("should fall back to the mercosul format when the argument is not a string", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generateLicensePlate(123)).toMatch(/^[A-Z]{3}\d[A-Z]\d{2}$/); }); diff --git a/src/generate-license-plate/generate-license-plate.ts b/src/generate-license-plate/generate-license-plate.ts index 60fb6083..723c3fdf 100644 --- a/src/generate-license-plate/generate-license-plate.ts +++ b/src/generate-license-plate/generate-license-plate.ts @@ -6,7 +6,7 @@ const DEFAULT_FORMAT = "LLLNLNN"; export type GenerateLicensePlateFormat = LicensePlateFormat; -const randomLetter = (): string => LETTERS[Math.floor(Math.random() * LETTERS.length)]; +const randomLetter = (): string => LETTERS.charAt(Math.floor(Math.random() * LETTERS.length)); const randomDigit = (): string => Math.floor(Math.random() * 10).toString(); @@ -34,8 +34,11 @@ export const generateLicensePlate = ( ): string => { const safeFormat = typeof format === "string" ? format : DEFAULT_FORMAT; - return safeFormat - .split("") - .map((char) => (char === "L" ? randomLetter() : randomDigit())) - .join(""); + let plate = ""; + + for (let i = 0; i < safeFormat.length; i++) { + plate += safeFormat.charAt(i) === "L" ? randomLetter() : randomDigit(); + } + + return plate; }; diff --git a/src/generate-passport/generate-passport.ts b/src/generate-passport/generate-passport.ts index 827beb01..05c905e1 100644 --- a/src/generate-passport/generate-passport.ts +++ b/src/generate-passport/generate-passport.ts @@ -6,7 +6,7 @@ import { ALPHABET_LENGTH, CHAR_CODE_A, DIGITS_LENGTH, LETTERS_LENGTH } from "./c * * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. * - * @returns A random valid passport number string (e.g. "RY393097"). + * @returns {string} A random valid passport number string (e.g. "RY393097"). * * @example * generatePassport() // "RY393097" diff --git a/src/generate-pis/generate-pis.ts b/src/generate-pis/generate-pis.ts index 6df9757f..43d0e0be 100644 --- a/src/generate-pis/generate-pis.ts +++ b/src/generate-pis/generate-pis.ts @@ -3,9 +3,10 @@ import { generateRandomNumber } from "../_internals/generate-random-number/gener import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; const calculateCheckDigit = (base: string): string => { - const sum = base - .split("") - .reduce((acc, digit, index) => acc + Number(digit) * PIS_WEIGHTS[index], 0); + const sum = PIS_WEIGHTS.reduce( + (acc, weight, index) => acc + Number(base.charAt(index)) * weight, + 0, + ); const digit = 11 - (sum % 11); return digit >= 10 ? "0" : digit.toString(); }; diff --git a/src/generate-pix-payload/generate-pix-payload.test.ts b/src/generate-pix-payload/generate-pix-payload.test.ts index 4d1d41fa..a6500444 100644 --- a/src/generate-pix-payload/generate-pix-payload.test.ts +++ b/src/generate-pix-payload/generate-pix-payload.test.ts @@ -17,21 +17,21 @@ const EVP = "71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d"; describe("generatePixPayload", () => { describe("should return null", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generatePixPayload(null)).toBeNull(); }); test("when it is undefined", () => { - // @ts-expect-error - expect(generatePixPayload(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(generatePixPayload()).toBeNull(); }); test("when it is not an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generatePixPayload("12345678909")).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generatePixPayload(123)).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generatePixPayload(true)).toBeNull(); }); @@ -62,7 +62,7 @@ describe("generatePixPayload", () => { test("when url is not a string", () => { expect( - // @ts-expect-error + // @ts-expect-error: intentionally invalid input generatePixPayload({ url: 123, merchantName: "Fulano", merchantCity: "Brasilia" }), ).toBeNull(); }); @@ -127,7 +127,7 @@ describe("generatePixPayload", () => { }); test('when it is a function, since a function is not typeof "object" even when it carries key/merchantName/merchantCity properties of its own', () => { - const impostor = Object.assign(() => {}, { + const impostor = Object.assign(() => null, { key: "12345678909", merchantName: "Fulano", merchantCity: "Brasilia", @@ -140,13 +140,13 @@ describe("generatePixPayload", () => { const impostor = { length: 5, toString: () => "pix.example.com/x" }; expect( - // @ts-expect-error + // @ts-expect-error: intentionally invalid input generatePixPayload({ url: impostor, merchantName: "Fulano", merchantCity: "Brasilia" }), ).toBeNull(); }); test("when the merchant name is missing or empty after folding", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generatePixPayload({ key: EVP, merchantCity: "Brasilia" })).toBeNull(); expect( generatePixPayload({ key: EVP, merchantName: " ", merchantCity: "Brasilia" }), @@ -157,7 +157,7 @@ describe("generatePixPayload", () => { }); test("when the merchant city is missing or empty after folding", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generatePixPayload({ key: EVP, merchantName: "Fulano" })).toBeNull(); expect( generatePixPayload({ key: EVP, merchantName: "Fulano", merchantCity: " " }), @@ -169,12 +169,12 @@ describe("generatePixPayload", () => { expect(generatePixPayload({ ...BASE, amount: -1 })).toBeNull(); expect(generatePixPayload({ ...BASE, amount: Number.NaN })).toBeNull(); expect(generatePixPayload({ ...BASE, amount: Number.POSITIVE_INFINITY })).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generatePixPayload({ ...BASE, amount: "10" })).toBeNull(); }); test("when the amount does not fit in 13 characters", () => { - expect(generatePixPayload({ ...BASE, amount: 12_345_678_901_2 })).toBeNull(); + expect(generatePixPayload({ ...BASE, amount: 123_456_789_012 })).toBeNull(); }); test("but accept an amount whose formatted length is exactly 13 characters", () => { @@ -187,7 +187,7 @@ describe("generatePixPayload", () => { expect(generatePixPayload({ ...BASE, txid: "Um-Id-Qualquer" })).toBeNull(); expect(generatePixPayload({ ...BASE, txid: "" })).toBeNull(); expect(generatePixPayload({ ...BASE, txid: "a".repeat(26) })).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generatePixPayload({ ...BASE, txid: 123 })).toBeNull(); }); }); @@ -314,9 +314,11 @@ describe("generatePixPayload", () => { expect( parsePixPayload(generatePixPayload({ ...BASE, key: " Fulano@Example.COM " }) ?? "")?.key, ).toBe("fulano@example.com"); - expect( - parsePixPayload(generatePixPayload({ ...BASE, key: EVP.toUpperCase() }) ?? "")?.key, - ).toBe(EVP); + const upperCaseEvp = EVP.toUpperCase(); + + expect(parsePixPayload(generatePixPayload({ ...BASE, key: upperCaseEvp }) ?? "")?.key).toBe( + EVP, + ); }); test("truncating the description to what the 99 character template leaves", () => { diff --git a/src/generate-pix-payload/generate-pix-payload.ts b/src/generate-pix-payload/generate-pix-payload.ts index f1b0c7fa..a92f4d56 100644 --- a/src/generate-pix-payload/generate-pix-payload.ts +++ b/src/generate-pix-payload/generate-pix-payload.ts @@ -62,6 +62,57 @@ export type GeneratePixPayloadParams = { const toAsciiField = (value: unknown, maxLength: number): string => typeof value === "string" ? sanitizeToAscii(value).slice(0, maxLength).trim() : ""; +type PixIdentifier = { + identifierId: string; + identifierValue: string; + pointOfInitiation: string | undefined; +}; + +const resolveIdentifier = ( + keyInput: string | undefined, + urlInput: string | undefined, +): PixIdentifier | null => { + if (keyInput === undefined) { + const url = urlInput; + + if (typeof url !== "string" || url.length > PIX_URL_MAX_LENGTH || !isValidPixUrl(url)) + return null; + + return { + identifierId: PIX_URL_ID, + identifierValue: url, + pointOfInitiation: PIX_DYNAMIC_POINT_OF_INITIATION, + }; + } + + const key = parsePixKey(keyInput); + + if (!key) return null; + + return { identifierId: PIX_KEY_ID, identifierValue: key.value, pointOfInitiation: undefined }; +}; + +const resolveFormattedAmount = ( + amount: number | undefined, + txid: string | undefined, + pointOfInitiation: string | undefined, +): string | null => { + if (pointOfInitiation !== undefined && (amount !== undefined || txid !== undefined)) return null; + + // Stryker disable next-line EqualityOperator: amount <= 0 differs from amount < 0 only at 0 (or -0), and both format to "0.00", which the "rounds to 0.00" check below always rejects anyway + if (amount !== undefined && (!Number.isFinite(amount) || amount <= 0)) return null; + + const formattedAmount = amount === undefined ? "" : amount.toFixed(AMOUNT_DECIMAL_PLACES); + + if (formattedAmount.length > PIX_TRANSACTION_AMOUNT_MAX_LENGTH) return null; + + if (amount !== undefined && Number(formattedAmount) === 0) return null; + + if (txid !== undefined && (typeof txid !== "string" || !TXID_REGEX.test(txid))) return null; + + return formattedAmount; +}; + /** * Generates the payload of a Pix BR Code, the string behind a Pix QR Code and behind "Pix * copia e cola". @@ -128,27 +179,11 @@ export const generatePixPayload = (params: GeneratePixPayloadParams): string | n if ((keyInput !== undefined) === (urlInput !== undefined)) return null; - let identifierId: string; - let identifierValue: string; - let pointOfInitiation: string | undefined; - - if (keyInput !== undefined) { - const key = parsePixKey(keyInput); + const identifier = resolveIdentifier(keyInput, urlInput); - if (!key) return null; + if (identifier === null) return null; - identifierId = PIX_KEY_ID; - identifierValue = key.value; - } else { - const url = urlInput; - - if (typeof url !== "string" || url.length > PIX_URL_MAX_LENGTH || !isValidPixUrl(url)) - return null; - - identifierId = PIX_URL_ID; - identifierValue = url; - pointOfInitiation = PIX_DYNAMIC_POINT_OF_INITIATION; - } + const { identifierId, identifierValue, pointOfInitiation } = identifier; const merchantName = toAsciiField(params.merchantName, PIX_MERCHANT_NAME_MAX_LENGTH); @@ -160,18 +195,9 @@ export const generatePixPayload = (params: GeneratePixPayloadParams): string | n const { amount, txid } = params; - if (pointOfInitiation !== undefined && (amount !== undefined || txid !== undefined)) return null; - - // Stryker disable next-line EqualityOperator: amount <= 0 differs from amount < 0 only at 0 (or -0), and both format to "0.00", which the "rounds to 0.00" check below always rejects anyway - if (amount !== undefined && (!Number.isFinite(amount) || amount <= 0)) return null; - - const formattedAmount = amount === undefined ? "" : amount.toFixed(AMOUNT_DECIMAL_PLACES); + const formattedAmount = resolveFormattedAmount(amount, txid, pointOfInitiation); - if (formattedAmount.length > PIX_TRANSACTION_AMOUNT_MAX_LENGTH) return null; - - if (amount !== undefined && Number(formattedAmount) === 0) return null; - - if (txid !== undefined && (typeof txid !== "string" || !TXID_REGEX.test(txid))) return null; + if (formattedAmount === null) return null; const gui = formatTlv({ id: PIX_GUI_ID, value: PIX_GUI }); const identifierObject = formatTlv({ id: identifierId, value: identifierValue }); @@ -191,9 +217,9 @@ export const generatePixPayload = (params: GeneratePixPayloadParams): string | n const payload = formatTlv({ id: PIX_PAYLOAD_FORMAT_INDICATOR_ID, value: PIX_PAYLOAD_FORMAT_INDICATOR }) + - (pointOfInitiation - ? formatTlv({ id: PIX_POINT_OF_INITIATION_ID, value: pointOfInitiation }) - : "") + + (pointOfInitiation === undefined + ? "" + : formatTlv({ id: PIX_POINT_OF_INITIATION_ID, value: pointOfInitiation })) + formatTlv({ id: PIX_MERCHANT_ACCOUNT_INFORMATION_ID, value: merchantAccountInformation }) + formatTlv({ id: PIX_MERCHANT_CATEGORY_CODE_ID, value: PIX_MERCHANT_CATEGORY_CODE }) + formatTlv({ id: PIX_TRANSACTION_CURRENCY_ID, value: PIX_TRANSACTION_CURRENCY }) + diff --git a/src/generate-processo-juridico/generate-processo-juridico.test.ts b/src/generate-processo-juridico/generate-processo-juridico.test.ts index c8d206bc..772ea8bd 100644 --- a/src/generate-processo-juridico/generate-processo-juridico.test.ts +++ b/src/generate-processo-juridico/generate-processo-juridico.test.ts @@ -26,7 +26,7 @@ describe("generateProcessoJuridico", () => { const value = generateProcessoJuridico({ year: currentYear, court: 5 }); expect(value).not.toBe(null); - expect((value as string).substring(9, 13)).toBe(String(currentYear)); + expect((value as string).slice(9, 13)).toBe(String(currentYear)); expect((value as string).charAt(13)).toBe("5"); expect(isValidProcessoJuridico(value as string)).toBe(true); }); @@ -57,9 +57,9 @@ describe("generateProcessoJuridico", () => { }); it("should return null when options is not an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generateProcessoJuridico("invalid")).toBe(null); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(generateProcessoJuridico(42)).toBe(null); }); diff --git a/src/generate-voter-id/generate-voter-id.test.ts b/src/generate-voter-id/generate-voter-id.test.ts index fed3ee11..9a06cedd 100644 --- a/src/generate-voter-id/generate-voter-id.test.ts +++ b/src/generate-voter-id/generate-voter-id.test.ts @@ -14,9 +14,9 @@ describe("generateVoterId", () => { }); it("should fall back to the default UF instead of throwing for an unknown state", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(() => generateVoterId("XX")).not.toThrow(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input const voterId = generateVoterId("XX"); expect(voterId.slice(8, 10)).toBe("28"); expect(isValidVoterId(voterId)).toBe(true); diff --git a/src/get-address-info-by-cep/get-address-info-by-cep.test.ts b/src/get-address-info-by-cep/get-address-info-by-cep.test.ts index 911748e1..7f855edf 100644 --- a/src/get-address-info-by-cep/get-address-info-by-cep.test.ts +++ b/src/get-address-info-by-cep/get-address-info-by-cep.test.ts @@ -16,7 +16,7 @@ type MockResponse = { const VALID_CEP = "01310100"; const VALID_CEP_MASKED = "01310-100"; -const LIVE_TEST_TIMEOUT = 15000; +const LIVE_TEST_TIMEOUT = 15_000; const viacepPayload = { bairro: "Bela Vista", @@ -46,7 +46,7 @@ const brasilApiPayload = { function createJsonResponse(payload: unknown, status = 200): MockResponse { return { - json: async () => payload, + json: () => Promise.resolve(payload), ok: status >= 200 && status < 300, status, }; @@ -56,7 +56,7 @@ function setupFetchMock( fetchMock: ReturnType, overrides?: Partial>, ) { - fetchMock.mockImplementation(async (input: string | URL | Request) => { + fetchMock.mockImplementation((input: string | URL | Request) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; if (url.includes("viacep.com.br")) { @@ -134,7 +134,9 @@ function shouldRunLiveCepTests() { if (typeof Deno !== "undefined") { return Deno.env.get("RUN_LIVE_CEP_TESTS") === "1"; } - } catch {} + } catch { + return false; + } try { const maybeProcess = ( @@ -145,10 +147,12 @@ function shouldRunLiveCepTests() { } ).process; - if (maybeProcess?.env?.RUN_LIVE_CEP_TESTS === "1") { + if (maybeProcess?.env?.["RUN_LIVE_CEP_TESTS"] === "1") { return true; } - } catch {} + } catch { + return false; + } return false; } @@ -229,7 +233,7 @@ describe("getAddressInfoByCep", () => { }); it("should accept valid CEP as number and pad with leading zeros", async () => { - const result = await getAddressInfoByCep(1310100); + const result = await getAddressInfoByCep(1_310_100); expectDefaultAddress(result); }); @@ -266,7 +270,7 @@ describe("getAddressInfoByCep", () => { it("should throw GetAddressInfoByCepValidationError for a providers array made only of inherited Object property names", async () => { await expect( getAddressInfoByCep(VALID_CEP, { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input providers: ["constructor", "toString"], }), ).rejects.toThrow(GetAddressInfoByCepValidationError); @@ -275,7 +279,7 @@ describe("getAddressInfoByCep", () => { it("should include the Portuguese message when providers filter down to none", async () => { await expect( getAddressInfoByCep(VALID_CEP, { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input providers: ["invalid"], }), ).rejects.toThrow("Nenhum provedor válido especificado"); @@ -299,7 +303,7 @@ describe("getAddressInfoByCep", () => { it("should filter out invalid provider names", async () => { const result = await getAddressInfoByCep(VALID_CEP, { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input providers: ["viacep", "invalid", "brasilapi"], }); @@ -491,6 +495,56 @@ describe("getAddressInfoByCep", () => { ); }); + it("should throw GetAddressInfoByCepNotFoundError when every provider returns a payload that is not an object", async () => { + setupFetchMock(fetchMock, { + brasilapi: createJsonResponse(null), + viacep: createJsonResponse("oops"), + widenet: createJsonResponse(42), + }); + + await expect( + getAddressInfoByCep(VALID_CEP, { providers: ["viacep", "brasilapi", "widenet"] }), + ).rejects.toThrow(GetAddressInfoByCepNotFoundError); + }); + + it("should throw GetAddressInfoByCepNotFoundError when ViaCEP returns an empty cep", async () => { + setupFetchMock(fetchMock, { viacep: createJsonResponse({ ...viacepPayload, cep: "" }) }); + + await expect(getAddressInfoByCep(VALID_CEP, { providers: ["viacep"] })).rejects.toThrow( + GetAddressInfoByCepNotFoundError, + ); + }); + + it("should throw GetAddressInfoByCepNotFoundError when Widenet returns an empty code", async () => { + setupFetchMock(fetchMock, { widenet: createJsonResponse({ ...widenetPayload, code: "" }) }); + + await expect(getAddressInfoByCep(VALID_CEP, { providers: ["widenet"] })).rejects.toThrow( + GetAddressInfoByCepNotFoundError, + ); + }); + + it("should throw GetAddressInfoByCepNotFoundError when BrasilAPI returns an empty cep", async () => { + setupFetchMock(fetchMock, { + brasilapi: createJsonResponse({ ...brasilApiPayload, cep: "" }), + }); + + await expect(getAddressInfoByCep(VALID_CEP, { providers: ["brasilapi"] })).rejects.toThrow( + GetAddressInfoByCepNotFoundError, + ); + }); + + it("should treat a field that is not a string as missing", async () => { + setupFetchMock(fetchMock, { + viacep: createJsonResponse({ ...viacepPayload, uf: 12, localidade: null }), + }); + + const result = await getAddressInfoByCep(VALID_CEP, { providers: ["viacep"] }); + + expect(result.state).toBe(""); + expect(result.city).toBe(""); + expect(result.street).toBe("Avenida Paulista"); + }); + it("should return first successful response when some providers fail", async () => { setupFetchMock(fetchMock, { brasilapi: createJsonResponse(brasilApiPayload), diff --git a/src/get-address-info-by-cep/get-address-info-by-cep.ts b/src/get-address-info-by-cep/get-address-info-by-cep.ts index 015dfbd7..e7b8c4df 100644 --- a/src/get-address-info-by-cep/get-address-info-by-cep.ts +++ b/src/get-address-info-by-cep/get-address-info-by-cep.ts @@ -3,28 +3,28 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d import { isValidCep } from "../is-valid-cep/is-valid-cep"; export class GetAddressInfoByCepError extends Error { - constructor(message: string) { + public constructor(message: string) { super(message); this.name = "GetAddressInfoByCepError"; } } export class GetAddressInfoByCepValidationError extends GetAddressInfoByCepError { - constructor(message: string) { + public constructor(message: string) { super(message); this.name = "GetAddressInfoByCepValidationError"; } } export class GetAddressInfoByCepNotFoundError extends GetAddressInfoByCepError { - constructor(message: string) { + public constructor(message: string) { super(message); this.name = "GetAddressInfoByCepNotFoundError"; } } export class GetAddressInfoByCepServiceError extends GetAddressInfoByCepError { - constructor(message: string) { + public constructor(message: string) { super(message); this.name = "GetAddressInfoByCepServiceError"; } @@ -50,34 +50,14 @@ export type GetAddressInfoByCepOptions = { providers?: CepProvider[]; }; -type ViaCepResponse = { - cep?: string; - logradouro?: string; - complemento?: string; - bairro?: string; - localidade?: string; - uf?: string; - erro?: boolean; -}; +type ProviderPayload = Record; -type WidenetResponse = { - code?: string; - status?: number; - ok?: boolean; - state?: string; - city?: string; - district?: string; - address?: string; - message?: string; -}; +const asString = (value: unknown): string => (typeof value === "string" ? value : ""); + +const readPayload = async (response: Response): Promise => { + const data: unknown = await response.json(); -type BrasilApiResponse = { - cep?: string; - state?: string; - city?: string; - neighborhood?: string; - street?: string; - errors?: Array<{ message: string }>; + return Object.assign({}, data); }; const fetchViaCep = async (cep: string): Promise => { @@ -89,20 +69,21 @@ const fetchViaCep = async (cep: string): Promise => { throw new Error(`ViaCEP request failed with status ${response.status}`); } - const data: ViaCepResponse = await response.json(); + const record = await readPayload(response); + const cepValue = asString(record["cep"]); - if (data.erro || !data.cep) { + if (Boolean(record["erro"]) || cepValue === "") { // Stryker disable next-line StringLiteral: only `instanceof GetAddressInfoByCepNotFoundError` // is checked when aggregating provider failures below, so this message is never observable. throw new GetAddressInfoByCepNotFoundError("CEP não encontrado"); } return { - cep: data.cep.replace(/\D/g, ""), - state: data.uf || "", - city: data.localidade || "", - neighborhood: data.bairro || "", - street: data.logradouro || "", + cep: cepValue.replaceAll(/\D/g, ""), + state: asString(record["uf"]), + city: asString(record["localidade"]), + neighborhood: asString(record["bairro"]), + street: asString(record["logradouro"]), }; }; @@ -117,20 +98,21 @@ const fetchWidenet = async (cep: string): Promise => { throw new Error(`Widenet request failed with status ${response.status}`); } - const data: WidenetResponse = await response.json(); + const record = await readPayload(response); + const codeValue = asString(record["code"]); - if (data.status !== 200 || !data.ok || !data.code) { + if (record["status"] !== 200 || record["ok"] !== true || codeValue === "") { // Stryker disable next-line StringLiteral: only `instanceof GetAddressInfoByCepNotFoundError` // is checked when aggregating provider failures below, so this message is never observable. throw new GetAddressInfoByCepNotFoundError("CEP não encontrado"); } return { - cep: data.code.replace(/\D/g, ""), - state: data.state || "", - city: data.city || "", - neighborhood: data.district || "", - street: data.address || "", + cep: codeValue.replaceAll(/\D/g, ""), + state: asString(record["state"]), + city: asString(record["city"]), + neighborhood: asString(record["district"]), + street: asString(record["address"]), }; }; @@ -143,20 +125,21 @@ const fetchBrasilApi = async (cep: string): Promise => { throw new Error(`BrasilAPI request failed with status ${response.status}`); } - const data: BrasilApiResponse = await response.json(); + const record = await readPayload(response); + const cepValue = asString(record["cep"]); - if (data.errors || !data.cep) { + if (Boolean(record["errors"]) || cepValue === "") { // Stryker disable next-line StringLiteral: only `instanceof GetAddressInfoByCepNotFoundError` // is checked when aggregating provider failures below, so this message is never observable. throw new GetAddressInfoByCepNotFoundError("CEP não encontrado"); } return { - cep: data.cep.replace(/\D/g, ""), - state: data.state || "", - city: data.city || "", - neighborhood: data.neighborhood || "", - street: data.street || "", + cep: cepValue.replaceAll(/\D/g, ""), + state: asString(record["state"]), + city: asString(record["city"]), + neighborhood: asString(record["neighborhood"]), + street: asString(record["street"]), }; }; @@ -215,45 +198,34 @@ export const getAddressInfoByCep = async ( } let providersToUse: CepProvider[]; - if (options?.providers !== undefined) { + if (options?.providers === undefined) { + providersToUse = ["viacep", "brasilapi"] as CepProvider[]; + } else { // An empty `options.providers` array also filters down to an empty `providersToUse` below, // which already reports the same validation error, so there is no dedicated check for it here. providersToUse = options.providers.filter((p) => Object.hasOwn(providerMap, p)); if (providersToUse.length === 0) { throw new GetAddressInfoByCepValidationError("Nenhum provedor válido especificado"); } - } else { - providersToUse = ["viacep", "brasilapi"] as CepProvider[]; } + let notFound = false; const providerPromises = providersToUse.map((provider) => - providerMap[provider](cepString).catch((error) => { - return Promise.reject({ provider, error }); + providerMap[provider](cepString).catch((error: unknown) => { + if (error instanceof GetAddressInfoByCepNotFoundError) notFound = true; + throw error; }), ); try { return await Promise.any(providerPromises); } catch { - const results = await Promise.allSettled(providerPromises); - - // Stryker disable next-line ConditionalExpression,MethodExpression: this line is only - // reached after `Promise.any` above has rejected, which by its contract only happens once - // every input promise has already rejected, so every result here is already "rejected"; the - // filter exists to narrow the element type from `PromiseSettledResult` to - // `PromiseRejectedResult` for the checks below, not to exclude anything at runtime. - const rejections = results.filter((result) => result.status === "rejected"); - - const networkErrors = rejections.filter( - (rejection) => !(rejection.reason.error instanceof GetAddressInfoByCepNotFoundError), - ); - - if (networkErrors.length === rejections.length) { - throw new GetAddressInfoByCepServiceError( - "Todos os serviços estão fora de serviço ou indisponíveis", - ); + if (notFound) { + throw new GetAddressInfoByCepNotFoundError("CEP não encontrado em nenhum serviço"); } - throw new GetAddressInfoByCepNotFoundError("CEP não encontrado em nenhum serviço"); + throw new GetAddressInfoByCepServiceError( + "Todos os serviços estão fora de serviço ou indisponíveis", + ); } }; diff --git a/src/get-area-code-info/get-area-code-info.test.ts b/src/get-area-code-info/get-area-code-info.test.ts index b65d6dfa..5b83c377 100644 --- a/src/get-area-code-info/get-area-code-info.test.ts +++ b/src/get-area-code-info/get-area-code-info.test.ts @@ -86,12 +86,12 @@ describe("getAreaCodeInfo", () => { }); it("should return null for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getAreaCodeInfo(null)).toBeNull(); }); it("should return null for undefined", () => { - // @ts-expect-error - expect(getAreaCodeInfo(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getAreaCodeInfo()).toBeNull(); }); }); diff --git a/src/get-area-code-info/get-area-code-info.ts b/src/get-area-code-info/get-area-code-info.ts index ce892ed2..94c35308 100644 --- a/src/get-area-code-info/get-area-code-info.ts +++ b/src/get-area-code-info/get-area-code-info.ts @@ -42,10 +42,10 @@ export const getAreaCodeInfo = (areaCode: string | number): AreaCodeInfo | null const numericAreaCode = Number(digits); - if (!(numericAreaCode in AREA_CODE_STATES)) return null; - const stateCode = AREA_CODE_STATES[numericAreaCode]; + if (stateCode === undefined) return null; + const statesByCode: Record = {}; for (const entry of DATA) statesByCode[entry.code] = entry; diff --git a/src/get-area-codes-by-state/get-area-codes-by-state.test.ts b/src/get-area-codes-by-state/get-area-codes-by-state.test.ts index c1a83a0c..96d89708 100644 --- a/src/get-area-codes-by-state/get-area-codes-by-state.test.ts +++ b/src/get-area-codes-by-state/get-area-codes-by-state.test.ts @@ -43,17 +43,17 @@ describe("getAreaCodesByState", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getAreaCodesByState(null)).toEqual([]); }); test("when it is undefined", () => { - // @ts-expect-error - expect(getAreaCodesByState(undefined)).toEqual([]); + // @ts-expect-error: intentionally invalid input + expect(getAreaCodesByState()).toEqual([]); }); test("when it is a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getAreaCodesByState(11)).toEqual([]); }); }); diff --git a/src/get-bank-by-code/get-bank-by-code.test.ts b/src/get-bank-by-code/get-bank-by-code.test.ts index ea525144..8bd70d3c 100644 --- a/src/get-bank-by-code/get-bank-by-code.test.ts +++ b/src/get-bank-by-code/get-bank-by-code.test.ts @@ -75,32 +75,32 @@ describe("getBankByCode", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByCode(null)).toBeNull(); }); test("when it is undefined", () => { - // @ts-expect-error - expect(getBankByCode(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getBankByCode()).toBeNull(); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByCode(true)).toBeNull(); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByCode({})).toBeNull(); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByCode([])).toBeNull(); }); test("when it is an array whose string form would otherwise resolve to a real code", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByCode([1])).toBeNull(); }); }); diff --git a/src/get-bank-by-ispb/get-bank-by-ispb.test.ts b/src/get-bank-by-ispb/get-bank-by-ispb.test.ts index 210d4051..55187c08 100644 --- a/src/get-bank-by-ispb/get-bank-by-ispb.test.ts +++ b/src/get-bank-by-ispb/get-bank-by-ispb.test.ts @@ -79,32 +79,32 @@ describe("getBankByIspb", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByIspb(null)).toBeNull(); }); test("when it is undefined", () => { - // @ts-expect-error - expect(getBankByIspb(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getBankByIspb()).toBeNull(); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByIspb(true)).toBeNull(); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByIspb({})).toBeNull(); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByIspb([])).toBeNull(); }); test("when it is an array whose string form would otherwise resolve to a real ispb", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getBankByIspb([0])).toBeNull(); }); }); diff --git a/src/get-banks/get-banks.test.ts b/src/get-banks/get-banks.test.ts index a7f9ff92..aa2db929 100644 --- a/src/get-banks/get-banks.test.ts +++ b/src/get-banks/get-banks.test.ts @@ -28,8 +28,16 @@ describe("getBanks", () => { }); it("should return fresh objects that do not affect subsequent calls when mutated", () => { - const banks = getBanks(); - banks[0].name = "mutated"; - expect(getBanks()[0].name).not.toBe("mutated"); + const firstBank = getBanks().at(0); + + expect(firstBank).toBeDefined(); + + if (firstBank === undefined) { + return; + } + + firstBank.name = "mutated"; + + expect(getBanks().at(0)?.name).not.toBe("mutated"); }); }); diff --git a/src/get-banks/get-banks.ts b/src/get-banks/get-banks.ts index 3e14c6cf..391d13fb 100644 --- a/src/get-banks/get-banks.ts +++ b/src/get-banks/get-banks.ts @@ -18,4 +18,4 @@ import { BANKS, type Bank } from "../_internals/constants/banks"; * @see Based on: https://brasilapi.com.br/api/banks/v1 Fallback source used by the dataset * generator (`scripts/banks.ts`) when the Bacen CSV request fails. */ -export const getBanks = (): Bank[] => BANKS.map((bank) => ({ ...bank })); +export const getBanks = (): Bank[] => BANKS.map((bank) => Object.assign({}, bank)); diff --git a/src/get-boleto-info/get-boleto-info.test.ts b/src/get-boleto-info/get-boleto-info.test.ts index aa921a2b..d57a7b81 100644 --- a/src/get-boleto-info/get-boleto-info.test.ts +++ b/src/get-boleto-info/get-boleto-info.test.ts @@ -30,7 +30,7 @@ describe("getBoletoInfo", () => { describe("should return boleto info", () => { test("when boleto is valid without mask", () => { expect(getBoletoInfo("00190000090114971860168524522114675860000102656")).toStrictEqual({ - amount: 102656, + amount: 102_656, expirationDate: new Date(2018, 6, 15), bankCode: "001", }); @@ -38,7 +38,7 @@ describe("getBoletoInfo", () => { test("when boleto is valid with mask", () => { expect(getBoletoInfo("0019000009 01149.718601 68524.522114 6 75860000102656")).toStrictEqual({ - amount: 102656, + amount: 102_656, expirationDate: new Date(2018, 6, 15), bankCode: "001", }); diff --git a/src/get-cbo/get-cbo.test.ts b/src/get-cbo/get-cbo.test.ts index 6b671b16..e2b964ff 100644 --- a/src/get-cbo/get-cbo.test.ts +++ b/src/get-cbo/get-cbo.test.ts @@ -17,14 +17,14 @@ describe("getCbo", () => { }); it("should return the occupation for a code given as a number", () => { - expect(getCbo(212405)).toEqual({ + expect(getCbo(212_405)).toEqual({ code: "212405", title: "Analista de desenvolvimento de sistemas", }); }); it("should pad a number to six digits so codes starting with zero resolve (0102-05, Oficial da Aeronáutica)", () => { - expect(getCbo(10205)).toEqual({ code: "010205", title: "Oficial da Aeronáutica" }); + expect(getCbo(10_205)).toEqual({ code: "010205", title: "Oficial da Aeronáutica" }); expect(getCbo("10205")).toBeNull(); }); @@ -51,7 +51,7 @@ describe("getCbo", () => { // @ts-expect-error not a string or number expect(getCbo(null)).toBeNull(); // @ts-expect-error not a string or number - expect(getCbo(undefined)).toBeNull(); + expect(getCbo()).toBeNull(); }); it("should return null for whitespace only", () => { diff --git a/src/get-cbo/get-cbo.ts b/src/get-cbo/get-cbo.ts index 44d4d760..237b7bec 100644 --- a/src/get-cbo/get-cbo.ts +++ b/src/get-cbo/get-cbo.ts @@ -37,7 +37,9 @@ export const getCbo = (value: string | number): Cbo | null => { const digits = typeof value === "number" ? String(value).padStart(6, "0") : sanitizeToDigits(value); - if (!(digits in CBO_TITLES)) return null; + const title = CBO_TITLES[digits]; - return { code: digits, title: CBO_TITLES[digits] }; + if (title === undefined) return null; + + return { code: digits, title }; }; diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.test.ts b/src/get-cep-info-by-address/get-cep-info-by-address.test.ts index 74a05ddb..357d4886 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.test.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.test.ts @@ -54,7 +54,7 @@ describe("getCepInfoByAddress", () => { }; const mockAddressListOnce = (addresses: unknown[]) => - fetchMock.mockResolvedValueOnce({ json: async () => addresses, ok: true }); + fetchMock.mockResolvedValueOnce({ json: () => Promise.resolve(addresses), ok: true }); it("should validate UF before fetching", async () => { await expect( @@ -87,14 +87,14 @@ describe("getCepInfoByAddress", () => { it("should build the URL from a trimmed, accent-stripped city and street", async () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: async () => [], + json: () => Promise.resolve([]), }); await getCepInfoByAddress({ federalUnit: "SP", city: " São Paulo ", street: " Àvenida Paulista ", - }).catch(() => undefined); + }).catch(() => null); const [url] = fetchMock.mock.calls[0]; expect(url).toBe( @@ -136,7 +136,7 @@ describe("getCepInfoByAddress", () => { it("should throw specifically GetCepInfoByAddressError (not a subclass) when the response is not ok", async () => { fetchMock.mockResolvedValueOnce({ - json: async () => ({}), + json: () => Promise.resolve({}), ok: false, status: 500, }); @@ -146,7 +146,9 @@ describe("getCepInfoByAddress", () => { city: "São Paulo", street: "Avenida Paulista", }).then( - () => undefined, + () => { + throw new Error("expected the request to reject"); + }, (error: unknown) => error, ); diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.ts b/src/get-cep-info-by-address/get-cep-info-by-address.ts index c0aba44f..2c483d59 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.ts @@ -3,21 +3,21 @@ import { fetchWithRetry } from "../_internals/fetch-with-retry/fetch-with-retry" import { removeAccents } from "../remove-accents/remove-accents"; export class GetCepInfoByAddressError extends Error { - constructor(message: string) { + public constructor(message: string) { super(message); this.name = "GetCepInfoByAddressError"; } } export class GetCepInfoByAddressValidationError extends GetCepInfoByAddressError { - constructor(message: string) { + public constructor(message: string) { super(message); this.name = "GetCepInfoByAddressValidationError"; } } export class GetCepInfoByAddressNotFoundError extends GetCepInfoByAddressError { - constructor(message: string) { + public constructor(message: string) { super(message); this.name = "GetCepInfoByAddressNotFoundError"; } @@ -60,6 +60,10 @@ const isStateCode = (value: string): value is StateCode => const normalizeAddressPart = (value: string): string => removeAccents(value).trim(); +// The ViaCEP response shape is trusted structurally (as the original implementation always +// was): every element the array holds is assumed to already match `CepAddressInfo`. +const isCepAddressInfoArray = (value: unknown): value is CepAddressInfo[] => Array.isArray(value); + /** * Looks every CEP of a Brazilian street up on the ViaCEP API. * @@ -105,9 +109,9 @@ export const getCepInfoByAddress = async ({ throw new GetCepInfoByAddressError(`ViaCEP request failed with status ${response.status}`); } - const data: CepAddressInfo[] = await response.json(); + const data: unknown = await response.json(); - if (!Array.isArray(data) || data.length === 0) { + if (!isCepAddressInfoArray(data) || data.length === 0) { throw new GetCepInfoByAddressNotFoundError(`${normalizedUf} - ${city} - ${street}`); } diff --git a/src/get-cfop/get-cfop.test.ts b/src/get-cfop/get-cfop.test.ts index 7386d98b..4e03cf1f 100644 --- a/src/get-cfop/get-cfop.test.ts +++ b/src/get-cfop/get-cfop.test.ts @@ -70,6 +70,6 @@ describe("getCfop", () => { it("should return null for undefined", () => { // @ts-expect-error not a string or number - expect(getCfop(undefined)).toBeNull(); + expect(getCfop()).toBeNull(); }); }); diff --git a/src/get-cfop/get-cfop.ts b/src/get-cfop/get-cfop.ts index f23a09b3..008c88c4 100644 --- a/src/get-cfop/get-cfop.ts +++ b/src/get-cfop/get-cfop.ts @@ -34,7 +34,9 @@ export const getCfop = (value: string | number): Cfop | null => { const digits = sanitizeToDigits(value); - if (!(digits in CFOP_TABLE)) return null; + const description = CFOP_TABLE[digits]; - return { code: digits, description: CFOP_TABLE[digits] }; + if (description === undefined) return null; + + return { code: digits, description }; }; diff --git a/src/get-cities/get-cities.test.ts b/src/get-cities/get-cities.test.ts index b8b4ae5c..1f670817 100644 --- a/src/get-cities/get-cities.test.ts +++ b/src/get-cities/get-cities.test.ts @@ -34,14 +34,14 @@ describe("getCities", () => { }); it("should return empty array if state does not exist", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getCities("ACC")).toEqual([]); }); it("should return empty array for inherited Object property names instead of throwing", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getCities("toString")).toEqual([]); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getCities("constructor")).toEqual([]); }); @@ -54,7 +54,7 @@ describe("getCities", () => { const spCities = getCities("SP"); spCities.push("MUTATED CITY"); - expect(getCities("SP").length).toEqual(KNOWN_STATE_CITY_COUNTS.SP); + expect(getCities("SP").length).toEqual(KNOWN_STATE_CITY_COUNTS["SP"]); }); describe("data integrity (IBGE 2022, https://cidades.ibge.gov.br/brasil/panorama)", () => { diff --git a/src/get-cnae/get-cnae.test.ts b/src/get-cnae/get-cnae.test.ts index 8387cfa8..8dcaa390 100644 --- a/src/get-cnae/get-cnae.test.ts +++ b/src/get-cnae/get-cnae.test.ts @@ -10,7 +10,7 @@ describe("getCnae", () => { }); it("should return the CNAE entry for a known code as a number", () => { - expect(getCnae(6201501)).toEqual({ + expect(getCnae(6_201_501)).toEqual({ code: "6201-5/01", description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA", }); @@ -24,7 +24,7 @@ describe("getCnae", () => { }); it("should pad a number to seven digits so codes starting with zero resolve (0111-3/01, cultivo de arroz)", () => { - expect(getCnae(111301)).toEqual({ code: "0111-3/01", description: "CULTIVO DE ARROZ" }); + expect(getCnae(111_301)).toEqual({ code: "0111-3/01", description: "CULTIVO DE ARROZ" }); expect(getCnae("111301")).toBeNull(); }); @@ -50,6 +50,6 @@ describe("getCnae", () => { // @ts-expect-error not a string or number expect(getCnae(null)).toBeNull(); // @ts-expect-error not a string or number - expect(getCnae(undefined)).toBeNull(); + expect(getCnae()).toBeNull(); }); }); diff --git a/src/get-cnae/get-cnae.ts b/src/get-cnae/get-cnae.ts index 6a8c3fc7..ecf4c498 100644 --- a/src/get-cnae/get-cnae.ts +++ b/src/get-cnae/get-cnae.ts @@ -36,7 +36,9 @@ export const getCnae = (value: string | number): Cnae | null => { const digits = typeof value === "number" ? String(value).padStart(7, "0") : sanitizeToDigits(value); - if (!(digits in CNAE_SUBCLASSES)) return null; + const description = CNAE_SUBCLASSES[digits]; - return { code: formatCnae(digits), description: CNAE_SUBCLASSES[digits] }; + if (description === undefined) return null; + + return { code: formatCnae(digits), description }; }; diff --git a/src/get-holidays/get-holidays.test.ts b/src/get-holidays/get-holidays.test.ts index 0634a7d8..315e1450 100644 --- a/src/get-holidays/get-holidays.test.ts +++ b/src/get-holidays/get-holidays.test.ts @@ -19,9 +19,9 @@ describe("getHolidays", () => { { name: "Natal", date: new Date(year, 11, 25), type: "national" }, ]; - fixedHolidays.forEach((holiday) => { + for (const holiday of fixedHolidays) { expect(holidays).toContainEqual(holiday); - }); + } }); test("should not include Dia da Consciência Negra as a national holiday before 2024 (Lei nº 14.759/2023 made it national only from 2024 onward)", () => { @@ -46,9 +46,9 @@ describe("getHolidays", () => { { name: "Corpus Christi", date: new Date(2031, 5, 12), type: "optional" }, ]; - expectedHolidays.forEach((holiday) => { + for (const holiday of expectedHolidays) { expect(holidays).toContainEqual(holiday); - }); + } }); test("should return 13 holidays for 2024: 9 fixed holidays (including Consciência Negra) plus 4 Easter-related holidays", () => { @@ -65,13 +65,13 @@ describe("getHolidays", () => { { year: 2075, month: 3, day: 7 }, ]; - easterSundays.forEach(({ year, month, day }) => { + for (const { year, month, day } of easterSundays) { expect(getHolidays(year)).toContainEqual({ name: "Páscoa", date: new Date(year, month, day), type: "religious", }); - }); + } }); test("should compute holidays for the inclusive boundary years 1900 and 2099", () => { @@ -93,19 +93,27 @@ describe("getHolidays", () => { expect(holidays.length).toBeGreaterThan(1); for (let index = 1; index < holidays.length; index += 1) { - expect(holidays[index].date.getTime()).toBeGreaterThanOrEqual( - holidays[index - 1].date.getTime(), - ); + const current = holidays.at(index); + const previous = holidays.at(index - 1); + + expect(current).toBeDefined(); + expect(previous).toBeDefined(); + + if (current === undefined || previous === undefined) { + continue; + } + + expect(current.date.getTime()).toBeGreaterThanOrEqual(previous.date.getTime()); } }); test("should return an empty array when called with null instead of a year or options object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getHolidays(null)).toEqual([]); }); test('should return an empty array when called with a function, even one carrying a year property (typeof yearOrOptions !== "object" must reject it, not just isNullish)', () => { - const fakeOptions = Object.assign(() => {}, { year: 2024 }); + const fakeOptions = Object.assign(() => null, { year: 2024 }); expect(getHolidays(fakeOptions)).toEqual([]); }); @@ -113,15 +121,15 @@ describe("getHolidays", () => { test("should return an empty array for a year that is not a valid supported integer", () => { const invalidYears = ["2024", 2024.5, 1899, 2100, Number.NaN]; - invalidYears.forEach((year) => { - // @ts-expect-error + for (const year of invalidYears) { + // @ts-expect-error: intentionally invalid input expect(getHolidays({ year })).toEqual([]); - }); + } }); test("should ignore a non-primitive (String object) stateCode and return national-only holidays", () => { const nationalHolidays = getHolidays(2024); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input const holidays = getHolidays({ year: 2024, stateCode: new String("SP") }); expect(holidays).toEqual(nationalHolidays); @@ -130,8 +138,15 @@ describe("getHolidays", () => { test("should compute independent results per year instead of colliding on a shared cache key", () => { const first = getHolidays(2081); const second = getHolidays(2082); + const firstOfSecond = second.at(0); + + expect(firstOfSecond).toBeDefined(); - expect(second[0].date.getFullYear()).toBe(2082); + if (firstOfSecond === undefined) { + return; + } + + expect(firstOfSecond.date.getFullYear()).toBe(2082); expect(second).not.toEqual(first); }); @@ -240,9 +255,9 @@ describe("getHolidays", () => { const nationalHolidays = getHolidays(2024); const spHolidays = getHolidays({ year: 2024, stateCode: "SP" }); - nationalHolidays.forEach((nationalHoliday) => { + for (const nationalHoliday of nationalHolidays) { expect(spHolidays).toContainEqual(nationalHoliday); - }); + } expect(spHolidays.length).toBeGreaterThan(nationalHolidays.length); }); @@ -258,7 +273,7 @@ describe("getHolidays", () => { test("should ignore an unknown stateCode and return national-only holidays", () => { const nationalHolidays = getHolidays(2024); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input const holidays = getHolidays({ year: 2024, stateCode: "XX" }); expect(holidays).toEqual(nationalHolidays); @@ -266,20 +281,34 @@ describe("getHolidays", () => { test("should ignore a non-string stateCode and return national-only holidays", () => { const nationalHolidays = getHolidays(2024); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input const holidays = getHolidays({ year: 2024, stateCode: 123 }); expect(holidays).toEqual(nationalHolidays); }); test("should return a fresh copy on every call so mutation cannot leak between calls", () => { - const first = getHolidays(2024); - first[0].name = "MUTATED"; - first[0].date.setFullYear(1900); + const firstHoliday = getHolidays(2024).at(0); + + expect(firstHoliday).toBeDefined(); + + if (firstHoliday === undefined) { + return; + } - const second = getHolidays(2024); - expect(second[0].name).not.toBe("MUTATED"); - expect(second[0].date.getFullYear()).toBe(2024); + firstHoliday.name = "MUTATED"; + firstHoliday.date.setFullYear(1900); + + const secondHoliday = getHolidays(2024).at(0); + + expect(secondHoliday).toBeDefined(); + + if (secondHoliday === undefined) { + return; + } + + expect(secondHoliday.name).not.toBe("MUTATED"); + expect(secondHoliday.date.getFullYear()).toBe(2024); }); test("should keep Nossa Senhora da Conceição for AM as an optional day, as the state calendar decree does", () => { const holiday = getHolidays({ year: 2024, stateCode: "AM" }).find( @@ -389,7 +418,7 @@ describe("getHolidays", () => { { name: "Feriado com apenas o dia", day: 10 }, ]; - incompleteEntries.forEach((entry) => { + for (const entry of incompleteEntries) { const entries = STATE_HOLIDAYS.AC ?? []; entries.push(entry); @@ -400,6 +429,6 @@ describe("getHolidays", () => { } finally { entries.pop(); } - }); + } }); }); diff --git a/src/get-holidays/get-holidays.ts b/src/get-holidays/get-holidays.ts index ddce37fc..1758675a 100644 --- a/src/get-holidays/get-holidays.ts +++ b/src/get-holidays/get-holidays.ts @@ -97,29 +97,28 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida const easterDate = calculateEaster(year); - holidays.push({ - name: "Carnaval (terça-feira)", - date: calculateHolidayFromEaster(year, -47), - type: "optional", - }); - - holidays.push({ - name: "Sexta-feira Santa", - date: calculateHolidayFromEaster(year, -2), - type: "national", - }); - - holidays.push({ - name: "Páscoa", - date: easterDate, - type: "religious", - }); - - holidays.push({ - name: "Corpus Christi", - date: calculateHolidayFromEaster(year, 60), - type: "optional", - }); + holidays.push( + { + name: "Carnaval (terça-feira)", + date: calculateHolidayFromEaster(year, -47), + type: "optional", + }, + { + name: "Sexta-feira Santa", + date: calculateHolidayFromEaster(year, -2), + type: "national", + }, + { + name: "Páscoa", + date: easterDate, + type: "religious", + }, + { + name: "Corpus Christi", + date: calculateHolidayFromEaster(year, 60), + type: "optional", + }, + ); // Stryker disable next-line ConditionalExpression: when stateCode is undefined, STATE_HOLIDAYS[stateCode] resolves to undefined too, so the inner `if (stateHolidays)` already no-ops either way if (stateCode !== undefined) { @@ -193,10 +192,9 @@ export function getHolidays(yearOrOptions: number | GetHolidaysOptions): Holiday // Stryker disable next-line BlockStatement: an empty block here still falls through to the `!Number.isInteger(year)` guard below, which returns [] anyway since `year` stays unassigned (undefined) if (isNullish(yearOrOptions) || typeof yearOrOptions !== "object") { return []; - } else { - year = yearOrOptions.year; - stateCode = yearOrOptions.stateCode; } + year = yearOrOptions.year; + stateCode = yearOrOptions.stateCode; } if (!Number.isInteger(year) || year < HOLIDAYS_MIN_YEAR || year > HOLIDAYS_MAX_YEAR) { diff --git a/src/get-legal-nature/get-legal-nature.test.ts b/src/get-legal-nature/get-legal-nature.test.ts index a363993b..79d97d7a 100644 --- a/src/get-legal-nature/get-legal-nature.test.ts +++ b/src/get-legal-nature/get-legal-nature.test.ts @@ -57,6 +57,6 @@ describe("getLegalNature", () => { it("should return null for undefined", () => { // @ts-expect-error not a string or number - expect(getLegalNature(undefined)).toBeNull(); + expect(getLegalNature()).toBeNull(); }); }); diff --git a/src/get-legal-natures/get-legal-natures.ts b/src/get-legal-natures/get-legal-natures.ts index 6b18b36d..cf6ca5a2 100644 --- a/src/get-legal-natures/get-legal-natures.ts +++ b/src/get-legal-natures/get-legal-natures.ts @@ -12,15 +12,4 @@ import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 */ -export const getLegalNatures = (): Record => { - const entries = Object.entries(LEGAL_NATURE); - - const result: Record = {}; - - for (let i = 0; i < entries.length; i++) { - const [code, description] = entries[i]; - result[code] = description; - } - - return result; -}; +export const getLegalNatures = (): Record => ({ ...LEGAL_NATURE }); diff --git a/src/get-municipalities/get-municipalities.test.ts b/src/get-municipalities/get-municipalities.test.ts index 9371c64a..f385b651 100644 --- a/src/get-municipalities/get-municipalities.test.ts +++ b/src/get-municipalities/get-municipalities.test.ts @@ -64,10 +64,17 @@ describe("getMunicipalities", () => { expect(getMunicipalities().length).toBe(NUMBER_OF_BRAZILIAN_MUNICIPALITIES); - const spMunicipalities = getMunicipalities("SP"); - spMunicipalities[0].name = "MUTATED"; + const firstSpMunicipality = getMunicipalities("SP").at(0); - expect(getMunicipalities("SP")[0].name).not.toBe("MUTATED"); + expect(firstSpMunicipality).toBeDefined(); + + if (firstSpMunicipality === undefined) { + return; + } + + firstSpMunicipality.name = "MUTATED"; + + expect(getMunicipalities("SP").at(0)?.name).not.toBe("MUTATED"); }); describe("data integrity (IBGE, https://servicodados.ibge.gov.br/api/docs/localidades)", () => { diff --git a/src/get-municipality-by-code/get-municipality-by-code.test.ts b/src/get-municipality-by-code/get-municipality-by-code.test.ts index 9d35a5ab..cad63aae 100644 --- a/src/get-municipality-by-code/get-municipality-by-code.test.ts +++ b/src/get-municipality-by-code/get-municipality-by-code.test.ts @@ -11,7 +11,7 @@ describe("getMunicipalityByCode", () => { }); it("should return the municipality for a known code (number)", () => { - expect(getMunicipalityByCode(3550308)).toEqual({ + expect(getMunicipalityByCode(3_550_308)).toEqual({ code: "3550308", name: "São Paulo", stateCode: "SP", @@ -49,21 +49,21 @@ describe("getMunicipalityByCode", () => { it("should return null for an object even if its string representation looks like a valid code", () => { expect( - // @ts-expect-error + // @ts-expect-error: intentionally invalid input getMunicipalityByCode({ toString: () => "3550308" }), ).toBeNull(); }); it("should return null for a non-string, non-number value", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getMunicipalityByCode(null)).toBeNull(); - // @ts-expect-error - expect(getMunicipalityByCode(undefined)).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input + expect(getMunicipalityByCode()).toBeNull(); + // @ts-expect-error: intentionally invalid input expect(getMunicipalityByCode(true)).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getMunicipalityByCode({})).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getMunicipalityByCode([])).toBeNull(); }); diff --git a/src/get-municipality/get-municipality.test.ts b/src/get-municipality/get-municipality.test.ts index dc339e66..e7aebc8a 100644 --- a/src/get-municipality/get-municipality.test.ts +++ b/src/get-municipality/get-municipality.test.ts @@ -74,10 +74,10 @@ describe("getMunicipality", () => { }); it("should return null for a non-string code", async () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input await expect(getMunicipality({ code: null })).resolves.toBeNull(); - // @ts-expect-error - await expect(getMunicipality({ code: 3550308 })).resolves.toBeNull(); + // @ts-expect-error: intentionally invalid input + await expect(getMunicipality({ code: 3_550_308 })).resolves.toBeNull(); }); it("should return null for a code with the wrong number of digits", async () => { @@ -92,26 +92,26 @@ describe("getMunicipality", () => { describe("options validation (non-object input)", () => { it("should return null for null", async () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input await expect(getMunicipality(null)).resolves.toBeNull(); }); it("should return null for undefined", async () => { - // @ts-expect-error - await expect(getMunicipality(undefined)).resolves.toBeNull(); + // @ts-expect-error: intentionally invalid input + await expect(getMunicipality()).resolves.toBeNull(); }); it("should return null for a primitive", async () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input await expect(getMunicipality("3550308")).resolves.toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input await expect(getMunicipality(123)).resolves.toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input await expect(getMunicipality(true)).resolves.toBeNull(); }); it("should return null for an array", async () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input await expect(getMunicipality([])).resolves.toBeNull(); }); }); @@ -122,12 +122,12 @@ describe("getMunicipality", () => { }); it("should return null for a non-string municipality name", async () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input await expect(getMunicipality({ municipalityName: null, uf: "SP" })).resolves.toBeNull(); }); it("should return null for a non-string uf", async () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input const options: GetMunicipalityByNameOptions = { municipalityName: "São Paulo", uf: null }; await expect(getMunicipality(options)).resolves.toBeNull(); diff --git a/src/get-municipality/get-municipality.ts b/src/get-municipality/get-municipality.ts index a1d76b6c..dfec7319 100644 --- a/src/get-municipality/get-municipality.ts +++ b/src/get-municipality/get-municipality.ts @@ -84,16 +84,16 @@ const getMunicipalityCodeByName = ({ * * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades */ -export const getMunicipality = async ( +export const getMunicipality = ( options: GetMunicipalityOptions, ): Promise<[string, string] | null | string> => { if (isNullish(options) || typeof options !== "object" || Array.isArray(options)) { - return null; + return Promise.resolve(null); } if ("code" in options) { - return getMunicipalityByCode(options.code); + return Promise.resolve(getMunicipalityByCode(options.code)); } - return getMunicipalityCodeByName(options); + return Promise.resolve(getMunicipalityCodeByName(options)); }; diff --git a/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts b/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts index 762bf5c2..fbe51334 100644 --- a/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts +++ b/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts @@ -55,13 +55,13 @@ describe("getStateByIbgeCode", () => { }); it("should return null for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getStateByIbgeCode(null)).toBeNull(); }); it("should return null for undefined", () => { - // @ts-expect-error - expect(getStateByIbgeCode(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getStateByIbgeCode()).toBeNull(); }); it("should ignore non-digit characters around the code", () => { diff --git a/src/get-state-code-by-name/get-state-code-by-name.test.ts b/src/get-state-code-by-name/get-state-code-by-name.test.ts index 8c5b963f..278137db 100644 --- a/src/get-state-code-by-name/get-state-code-by-name.test.ts +++ b/src/get-state-code-by-name/get-state-code-by-name.test.ts @@ -49,17 +49,17 @@ describe("getStateCodeByName", () => { }); it("should return null for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getStateCodeByName(null)).toBeNull(); }); it("should return null for undefined", () => { - // @ts-expect-error - expect(getStateCodeByName(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getStateCodeByName()).toBeNull(); }); it("should return null for a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getStateCodeByName(35)).toBeNull(); }); }); diff --git a/src/get-state-name-by-code/get-state-name-by-code.test.ts b/src/get-state-name-by-code/get-state-name-by-code.test.ts index e9f7ff6b..c6d219dd 100644 --- a/src/get-state-name-by-code/get-state-name-by-code.test.ts +++ b/src/get-state-name-by-code/get-state-name-by-code.test.ts @@ -36,17 +36,17 @@ describe("getStateNameByCode", () => { }); it("should return null for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getStateNameByCode(null)).toBeNull(); }); it("should return null for undefined", () => { - // @ts-expect-error - expect(getStateNameByCode(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getStateNameByCode()).toBeNull(); }); it("should return null for a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getStateNameByCode(11)).toBeNull(); }); }); diff --git a/src/get-states/get-states.test.ts b/src/get-states/get-states.test.ts index 2ef7291a..9f8e98ad 100644 --- a/src/get-states/get-states.test.ts +++ b/src/get-states/get-states.test.ts @@ -49,12 +49,19 @@ describe("getStates", () => { }); it("should return unique deep copies so mutating the result does not leak between calls", () => { - const first = getStates(); + const firstState = getStates().at(0); - Object.assign(first[0], { name: "X" }); + expect(firstState).toBeDefined(); + + if (firstState === undefined) { + return; + } + + Object.assign(firstState, { name: "X" }); const second = getStates(); - expect(second[0].name).not.toBe("X"); - expect(second).toEqual(DATA.map((state) => ({ ...state }))); + + expect(second.at(0)?.name).not.toBe("X"); + expect(second).toEqual(DATA.map((state) => Object.assign({}, state))); }); }); diff --git a/src/get-states/get-states.ts b/src/get-states/get-states.ts index 7f85c11b..3125fdc5 100644 --- a/src/get-states/get-states.ts +++ b/src/get-states/get-states.ts @@ -21,4 +21,4 @@ import { DATA, type State } from "../_internals/constants/states"; * * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades */ -export const getStates = (): State[] => DATA.map((state) => ({ ...state })); +export const getStates = (): State[] => DATA.map((state) => Object.assign({}, state)); diff --git a/src/get-timezone-by-state/get-timezone-by-state.test.ts b/src/get-timezone-by-state/get-timezone-by-state.test.ts index d75edb2a..a5d4446e 100644 --- a/src/get-timezone-by-state/get-timezone-by-state.test.ts +++ b/src/get-timezone-by-state/get-timezone-by-state.test.ts @@ -91,17 +91,17 @@ describe("getTimezoneByState", () => { }); it("should return null for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getTimezoneByState(null)).toBeNull(); }); it("should return null for undefined", () => { - // @ts-expect-error - expect(getTimezoneByState(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getTimezoneByState()).toBeNull(); }); it("should return null for a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(getTimezoneByState(35)).toBeNull(); }); diff --git a/src/index.test.ts b/src/index.test.ts index 2403535f..fc0734df 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -225,7 +225,7 @@ const PUBLIC = [ const NETWORK_ENTRY_POINTS = new Set(["getAddressInfoByCep", "getCepInfoByAddress"]); -const BAD_INPUTS: Array<[string, unknown]> = [ +const BAD_INPUTS: [string, unknown][] = [ ["null", null], ["undefined", undefined], ["a number", 123], @@ -336,7 +336,7 @@ describe("Public API contract: never throws on bad input", () => { const entries = Object.entries(brazilianUtils).filter( ([name, value]) => typeof value === "function" && !isErrorClass(name) && !NETWORK_ENTRY_POINTS.has(name), - ) as Array<[string, (...args: unknown[]) => unknown]>; + ) as [string, (...args: unknown[]) => unknown][]; for (const [name, fn] of entries) { for (const [label, value] of BAD_INPUTS) { diff --git a/src/is-business-day/is-business-day.test.ts b/src/is-business-day/is-business-day.test.ts index 89cea575..67f6559b 100644 --- a/src/is-business-day/is-business-day.test.ts +++ b/src/is-business-day/is-business-day.test.ts @@ -38,7 +38,7 @@ describe("isBusinessDay", () => { }); it("should ignore an unknown stateCode and fall back to national holidays", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: "XX" })).toBe(true); }); }); @@ -77,18 +77,18 @@ describe("isBusinessDay", () => { }); it("should return false for a non-Date value", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isBusinessDay("2024-01-02")).toBe(false); }); it("should return false for null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isBusinessDay(null)).toBe(false); }); it("should return false for undefined", () => { - // @ts-expect-error - expect(isBusinessDay(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isBusinessDay()).toBe(false); }); }); diff --git a/src/is-business-day/is-business-day.ts b/src/is-business-day/is-business-day.ts index c60a50dc..cbee9172 100644 --- a/src/is-business-day/is-business-day.ts +++ b/src/is-business-day/is-business-day.ts @@ -9,7 +9,7 @@ export type IsBusinessDayOptions = { includeOptional?: boolean; }; -const WEEKEND_DAYS = [0, 6]; +const WEEKEND_DAYS = new Set([0, 6]); /** * Checks whether a given date is a Brazilian business day (dia útil). @@ -63,7 +63,7 @@ export const isBusinessDay = (value: Date, options?: IsBusinessDayOptions): bool if (year < HOLIDAYS_MIN_YEAR || year > HOLIDAYS_MAX_YEAR) return false; - if (WEEKEND_DAYS.includes(value.getDay())) return false; + if (WEEKEND_DAYS.has(value.getDay())) return false; const stateCode = options?.stateCode; const includeOptional = options?.includeOptional ?? true; diff --git a/src/is-holiday/is-holiday.test.ts b/src/is-holiday/is-holiday.test.ts index 3481708f..7e8f2152 100644 --- a/src/is-holiday/is-holiday.test.ts +++ b/src/is-holiday/is-holiday.test.ts @@ -31,18 +31,18 @@ describe("isHoliday", () => { }); it("should return false when targetDate is a string instead of a Date", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isHoliday({ targetDate: "2024-01-01" })).toBe(false); }); it('should return false when options is a function, even one carrying a targetDate property (typeof options !== "object" must reject it, not just isNullish)', () => { - const fakeOptions = Object.assign(() => {}, { targetDate: new Date(2024, 0, 1) }); + const fakeOptions = Object.assign(() => null, { targetDate: new Date(2024, 0, 1) }); expect(isHoliday(fakeOptions)).toBe(false); }); it("should return false when stateCode is not a string", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isHoliday({ targetDate: new Date(2024, 0, 1), stateCode: 123 })).toBe(false); }); @@ -51,9 +51,9 @@ describe("isHoliday", () => { }); it("should ignore an unknown stateCode and fall back to national holidays", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isHoliday({ targetDate: new Date(2024, 0, 1), stateCode: "XX" })).toBe(true); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isHoliday({ targetDate: new Date(2024, 5, 10), stateCode: "XX" })).toBe(false); }); diff --git a/src/is-holiday/is-holiday.ts b/src/is-holiday/is-holiday.ts index 721cac2d..053336f3 100644 --- a/src/is-holiday/is-holiday.ts +++ b/src/is-holiday/is-holiday.ts @@ -53,7 +53,8 @@ export const isHoliday = (options?: IsHolidayOptions): boolean => { return false; } - return getHolidays({ year: targetDate.getFullYear(), stateCode }).some((holiday) => { + const year = targetDate.getFullYear(); + return getHolidays({ year, stateCode }).some((holiday) => { return ( holiday.date.getMonth() === targetDate.getMonth() && holiday.date.getDate() === targetDate.getDate() diff --git a/src/is-valid-bank-account/is-valid-bank-account.test.ts b/src/is-valid-bank-account/is-valid-bank-account.test.ts index 2540bcf2..f417a1d6 100644 --- a/src/is-valid-bank-account/is-valid-bank-account.test.ts +++ b/src/is-valid-bank-account/is-valid-bank-account.test.ts @@ -13,17 +13,17 @@ const BANCO_DO_BRASIL_AGENCY_TOO_LONG_PARAMS = { describe("isValidBankAccount", () => { describe("should return false", () => { test("when params is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidBankAccount(null)).toBe(false); }); test("when params is undefined", () => { - // @ts-expect-error - expect(isValidBankAccount(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidBankAccount()).toBe(false); }); test("when params is not an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidBankAccount("001")).toBe(false); }); @@ -74,7 +74,7 @@ describe("isValidBankAccount", () => { test("when bankCode is null", () => { expect( isValidBankAccount({ - // @ts-expect-error + // @ts-expect-error: intentionally invalid input bankCode: null, agency: "1234", account: "12345678", @@ -86,7 +86,7 @@ describe("isValidBankAccount", () => { test("when bankCode is undefined", () => { expect( isValidBankAccount({ - // @ts-expect-error + // @ts-expect-error: intentionally invalid input bankCode: undefined, agency: "1234", account: "12345678", @@ -98,7 +98,7 @@ describe("isValidBankAccount", () => { test("when bankCode is not a string", () => { expect( isValidBankAccount({ - // @ts-expect-error + // @ts-expect-error: intentionally invalid input bankCode: 123, agency: "1234", account: "12345678", @@ -1274,7 +1274,7 @@ describe("isValidBankAccount", () => { test("should return false when bankCode is a truthy number that stringifies to a listed code", () => { expect( isValidBankAccount({ - // @ts-expect-error + // @ts-expect-error: intentionally invalid input bankCode: 246, agency: "1234", account: "123456", @@ -1287,7 +1287,7 @@ describe("isValidBankAccount", () => { expect( isValidBankAccount({ bankCode: "246", - // @ts-expect-error + // @ts-expect-error: intentionally invalid input agency: 1234, account: "123456", digit: "6", @@ -1300,8 +1300,8 @@ describe("isValidBankAccount", () => { isValidBankAccount({ bankCode: "246", agency: "1234", - // @ts-expect-error - account: 123456, + // @ts-expect-error: intentionally invalid input + account: 123_456, digit: "6", }), ).toBe(false); @@ -1313,14 +1313,14 @@ describe("isValidBankAccount", () => { bankCode: "246", agency: "1234", account: "123456", - // @ts-expect-error + // @ts-expect-error: intentionally invalid input digit: 6, }), ).toBe(false); }); test("should return false when params is a function carrying otherwise valid fields as own properties", () => { - const params = Object.assign(() => {}, { + const params = Object.assign(() => null, { bankCode: "001", agency: "1584", account: "00210169", diff --git a/src/is-valid-bank-account/is-valid-bank-account.ts b/src/is-valid-bank-account/is-valid-bank-account.ts index d12d4439..8b3aa4cb 100644 --- a/src/is-valid-bank-account/is-valid-bank-account.ts +++ b/src/is-valid-bank-account/is-valid-bank-account.ts @@ -49,10 +49,12 @@ const santanderDigits: BankAccountDigits = (agency, account) => { const base = `${agency}00${account}`; let sum = 0; + let position = 0; - for (let i = 0; i < base.length; i++) { + for (const weight of SANTANDER_WEIGHTS) { // Stryker disable next-line ArithmeticOperator: SANTANDER_WEIGHTS sums to 60, a multiple of 10, so replacing -48 with +48 shifts every term's contribution by a multiple of 10 mod 10, leaving the final check digit unchanged for every possible input. - sum += ((base.charCodeAt(i) - 48) * SANTANDER_WEIGHTS[i]) % 10; + sum += ((base.charCodeAt(position) - 48) * weight) % 10; + position++; } return [String((10 - (sum % 10)) % 10)]; @@ -229,7 +231,8 @@ const validateGeneric = (account: string, digit: string): boolean => { ); }; -const sanitizeCheckDigit = (value: string): string => value.toUpperCase().replace(/[^\dPX]/g, ""); +const sanitizeCheckDigit = (value: string): string => + value.toUpperCase().replaceAll(/[^\dPX]/g, ""); /** * Validates a Brazilian bank account. The bank code must belong to the Banco Central do Brasil diff --git a/src/is-valid-boleto/is-valid-boleto.test.ts b/src/is-valid-boleto/is-valid-boleto.test.ts index 2f185a7b..671041e8 100644 --- a/src/is-valid-boleto/is-valid-boleto.test.ts +++ b/src/is-valid-boleto/is-valid-boleto.test.ts @@ -9,13 +9,13 @@ describe("isValidBoleto", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidBoleto(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidBoleto(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidBoleto()).toBe(false); }); test(`when length is less than ${BOLETO_LENGTH}`, () => { @@ -23,19 +23,19 @@ describe("isValidBoleto", () => { }); test("when is array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidBoleto([])).toBe(false); }); test("when is object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidBoleto({})).toBe(false); }); test("when is boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidBoleto(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidBoleto(false)).toBe(false); }); diff --git a/src/is-valid-boleto/is-valid-boleto.ts b/src/is-valid-boleto/is-valid-boleto.ts index 449dac8a..dbcdc05c 100644 --- a/src/is-valid-boleto/is-valid-boleto.ts +++ b/src/is-valid-boleto/is-valid-boleto.ts @@ -8,7 +8,7 @@ import { CHECK_DIGIT_POSITION, CONVERT_POSITIONS, PARTIALS } from "./constants"; const isValidPartials = (digits: string): boolean => { for (const { start, end, checkIdx } of PARTIALS) { - const partial = digits.substring(start, end); + const partial = digits.slice(start, end); const expected = mod10(partial); if (digits.charCodeAt(checkIdx) - 48 !== expected) return false; } @@ -18,14 +18,14 @@ const isValidPartials = (digits: string): boolean => { const parseToBoleto = (digits: string): string => { let result = ""; for (const [start, end] of CONVERT_POSITIONS) { - result += digits.substring(start, end); + result += digits.slice(start, end); } return result; }; const isValidCheckDigit = (boleto: string): boolean => { const withoutCheckDigit = - boleto.substring(0, CHECK_DIGIT_POSITION) + boleto.substring(CHECK_DIGIT_POSITION + 1); + boleto.slice(0, CHECK_DIGIT_POSITION) + boleto.slice(CHECK_DIGIT_POSITION + 1); const expected = mod11(withoutCheckDigit); return boleto.charCodeAt(CHECK_DIGIT_POSITION) - 48 === expected; }; diff --git a/src/is-valid-caepf/is-valid-caepf.test.ts b/src/is-valid-caepf/is-valid-caepf.test.ts index f3f9d9c8..e9d5ba77 100644 --- a/src/is-valid-caepf/is-valid-caepf.test.ts +++ b/src/is-valid-caepf/is-valid-caepf.test.ts @@ -4,17 +4,17 @@ import { isValidCaepf } from "./is-valid-caepf"; describe("isValidCaepf", () => { describe("should return false", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCaepf(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCaepf(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCaepf()).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCaepf([])).toBe(false); }); @@ -74,7 +74,7 @@ describe("isValidCaepf", () => { }); test("for a number input", () => { - expect(isValidCaepf(29311861000184)).toBe(true); + expect(isValidCaepf(29_311_861_000_184)).toBe(true); }); test("for a whitespace mask and surrounding whitespace", () => { diff --git a/src/is-valid-cbo/is-valid-cbo.test.ts b/src/is-valid-cbo/is-valid-cbo.test.ts index 7943d4e6..79055010 100644 --- a/src/is-valid-cbo/is-valid-cbo.test.ts +++ b/src/is-valid-cbo/is-valid-cbo.test.ts @@ -11,7 +11,7 @@ describe("isValidCbo", () => { }); it("should validate a CBO code given as a number", () => { - expect(isValidCbo(212405)).toBe(true); + expect(isValidCbo(212_405)).toBe(true); }); it("should validate a CBO code with surrounding whitespace", () => { @@ -35,7 +35,7 @@ describe("isValidCbo", () => { // @ts-expect-error not a string or number expect(isValidCbo(null)).toBe(false); // @ts-expect-error not a string or number - expect(isValidCbo(undefined)).toBe(false); + expect(isValidCbo()).toBe(false); }); it("should return false for whitespace only", () => { diff --git a/src/is-valid-cei/is-valid-cei.test.ts b/src/is-valid-cei/is-valid-cei.test.ts index 28616e8e..cbbd4130 100644 --- a/src/is-valid-cei/is-valid-cei.test.ts +++ b/src/is-valid-cei/is-valid-cei.test.ts @@ -4,17 +4,17 @@ import { isValidCei } from "./is-valid-cei"; describe("isValidCei", () => { describe("should return false", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCei(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCei(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCei()).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCei(true)).toBe(false); }); @@ -78,7 +78,7 @@ describe("isValidCei", () => { }); test("for a number input", () => { - expect(isValidCei(249859674386)).toBe(true); + expect(isValidCei(249_859_674_386)).toBe(true); }); test("for a whitespace mask and surrounding whitespace", () => { diff --git a/src/is-valid-cep/is-valid-cep.test.ts b/src/is-valid-cep/is-valid-cep.test.ts index 2deb6b97..8e4696f2 100644 --- a/src/is-valid-cep/is-valid-cep.test.ts +++ b/src/is-valid-cep/is-valid-cep.test.ts @@ -8,17 +8,17 @@ describe("isValidCep", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCep(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCep(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCep()).toBe(false); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCep({})).toBe(false); }); @@ -51,7 +51,7 @@ describe("isValidCep", () => { }); test("when is a CEP valid as a number", () => { - expect(isValidCep(20040020)).toBe(true); + expect(isValidCep(20_040_020)).toBe(true); }); test("when is a CEP valid with leading/trailing whitespace", () => { diff --git a/src/is-valid-certidao/is-valid-certidao.test.ts b/src/is-valid-certidao/is-valid-certidao.test.ts index 6a827af4..e6bbadfc 100644 --- a/src/is-valid-certidao/is-valid-certidao.test.ts +++ b/src/is-valid-certidao/is-valid-certidao.test.ts @@ -4,17 +4,17 @@ import { isValidCertidao } from "./is-valid-certidao"; describe("isValidCertidao", () => { describe("should return false", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCertidao(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCertidao(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCertidao()).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCertidao([])).toBe(false); }); @@ -52,7 +52,7 @@ describe("isValidCertidao", () => { }); test("when it is a number, which cannot carry the 32 significant digits of a matrícula", () => { - expect(isValidCertidao(1045390155)).toBe(false); + expect(isValidCertidao(1_045_390_155)).toBe(false); }); }); diff --git a/src/is-valid-cfop/is-valid-cfop.test.ts b/src/is-valid-cfop/is-valid-cfop.test.ts index c7c149b8..6207f372 100644 --- a/src/is-valid-cfop/is-valid-cfop.test.ts +++ b/src/is-valid-cfop/is-valid-cfop.test.ts @@ -48,7 +48,7 @@ describe("isValidCfop", () => { it("should return false for undefined", () => { // @ts-expect-error not a string or number - expect(isValidCfop(undefined)).toBe(false); + expect(isValidCfop()).toBe(false); }); it("should return false for a non numeric string", () => { diff --git a/src/is-valid-cnae/is-valid-cnae.test.ts b/src/is-valid-cnae/is-valid-cnae.test.ts index 7764876f..aac9acb0 100644 --- a/src/is-valid-cnae/is-valid-cnae.test.ts +++ b/src/is-valid-cnae/is-valid-cnae.test.ts @@ -11,7 +11,7 @@ describe("isValidCnae", () => { }); it("should validate a CNAE code given as a number", () => { - expect(isValidCnae(6201501)).toBe(true); + expect(isValidCnae(6_201_501)).toBe(true); }); it("should validate a CNAE code with surrounding whitespace", () => { @@ -35,7 +35,7 @@ describe("isValidCnae", () => { // @ts-expect-error not a string or number expect(isValidCnae(null)).toBe(false); // @ts-expect-error not a string or number - expect(isValidCnae(undefined)).toBe(false); + expect(isValidCnae()).toBe(false); }); it("should return false for whitespace only", () => { diff --git a/src/is-valid-cnh/is-valid-cnh.test.ts b/src/is-valid-cnh/is-valid-cnh.test.ts index 70b55794..0626ee32 100644 --- a/src/is-valid-cnh/is-valid-cnh.test.ts +++ b/src/is-valid-cnh/is-valid-cnh.test.ts @@ -26,9 +26,9 @@ describe("isValidCnh", () => { it("should return false for falsy or non-string values", () => { expect(isValidCnh("")).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCnh(null)).toBe(false); - // @ts-expect-error - expect(isValidCnh(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCnh()).toBe(false); }); }); diff --git a/src/is-valid-cno/is-valid-cno.test.ts b/src/is-valid-cno/is-valid-cno.test.ts index 4ce6d5d6..29c59a54 100644 --- a/src/is-valid-cno/is-valid-cno.test.ts +++ b/src/is-valid-cno/is-valid-cno.test.ts @@ -4,17 +4,17 @@ import { isValidCno } from "./is-valid-cno"; describe("isValidCno", () => { describe("should return false", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCno(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCno(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCno()).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCno([])).toBe(false); }); @@ -65,7 +65,7 @@ describe("isValidCno", () => { test("for 401800097960, whose check digit is 0 (Receita Federal CNO open dataset, Frutal/MG)", () => { expect(isValidCno("401800097960")).toBe(true); - expect(isValidCno(401800097960)).toBe(true); + expect(isValidCno(401_800_097_960)).toBe(true); }); test("for 512070915160, whose check digit is 0 (Receita Federal CNO open dataset, Capitólio/MG)", () => { diff --git a/src/is-valid-cnpj/is-valid-cnpj.test.ts b/src/is-valid-cnpj/is-valid-cnpj.test.ts index 5a6a7c8c..70746183 100644 --- a/src/is-valid-cnpj/is-valid-cnpj.test.ts +++ b/src/is-valid-cnpj/is-valid-cnpj.test.ts @@ -17,29 +17,29 @@ describe("isValidCnpj", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCnpj(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCnpj(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCnpj()).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCnpj(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCnpj(false)).toBe(false); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCnpj({})).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCnpj([])).toBe(false); }); diff --git a/src/is-valid-cnpj/is-valid-cnpj.ts b/src/is-valid-cnpj/is-valid-cnpj.ts index e96254ae..9740ae48 100644 --- a/src/is-valid-cnpj/is-valid-cnpj.ts +++ b/src/is-valid-cnpj/is-valid-cnpj.ts @@ -18,12 +18,10 @@ const NUMERIC_FORMAT_REGEX = /^\d{2}[\s.\-/]*\d{3}[\s.\-/]*\d{3}[\s.\-/]*\d{4}[\ const cleanCnpj = (cnpj: string): string => { let result = ""; - // Stryker disable next-line EqualityOperator: cnpj.length is the exact bound; one extra iteration would read cnpj[cnpj.length], which is undefined and fails every character-class comparison below either way. - for (let i = 0; i < cnpj.length; i++) { + for (const char of cnpj) { // Stryker disable next-line ConditionalExpression,EqualityOperator: this early exit only bounds how much of an oversized input is scanned; whatever length `result` ends up with, the caller's FORMAT_REGEX/NUMERIC_FORMAT_REGEX check still requires exactly CNPJ_LENGTH real characters and rejects anything else, so the exact cutoff point here never changes the final answer. if (result.length > CNPJ_LENGTH) break; - const char = cnpj[i]; // Stryker disable next-line ConditionalExpression: the only characters that ever reach isValidChecksum are ones the caller's FORMAT_REGEX/NUMERIC_FORMAT_REGEX already restricted to "0"-"9", "A"-"Z" or a "\s.-/" separator (all below "0" in code point), so no reachable character can trigger this comparison's alternate branch without the whole match already having failed for an unrelated reason. const isDigit = char >= "0" && char <= "9"; // Stryker disable next-line ConditionalExpression: same reasoning as isDigit above — any character reaching here already satisfied FORMAT_REGEX/NUMERIC_FORMAT_REGEX, so it is always a genuine "0"-"9", "A"-"Z", "a"-"z" or a low-code-point separator. @@ -40,16 +38,20 @@ const cleanCnpj = (cnpj: string): string => { const isValidChecksum = (cnpj: string): boolean => { let sum = 0; - for (let i = 0; i < 12; i++) { - sum += (cnpj.charCodeAt(i) - 48) * CNPJ_FIRST_DIGIT_WEIGHTS[i]; + let position = 0; + for (const weight of CNPJ_FIRST_DIGIT_WEIGHTS) { + sum += (cnpj.charCodeAt(position) - 48) * weight; + position++; } let mod = sum % 11; const expected1 = mod < 2 ? 48 : 48 + 11 - mod; if (cnpj.charCodeAt(12) !== expected1) return false; sum = 0; - for (let i = 0; i < 13; i++) { - sum += (cnpj.charCodeAt(i) - 48) * CNPJ_SECOND_DIGIT_WEIGHTS[i]; + position = 0; + for (const weight of CNPJ_SECOND_DIGIT_WEIGHTS) { + sum += (cnpj.charCodeAt(position) - 48) * weight; + position++; } mod = sum % 11; const expected2 = mod < 2 ? 48 : 48 + 11 - mod; diff --git a/src/is-valid-cns/is-valid-cns.test.ts b/src/is-valid-cns/is-valid-cns.test.ts index 3d04e87f..085d1320 100644 --- a/src/is-valid-cns/is-valid-cns.test.ts +++ b/src/is-valid-cns/is-valid-cns.test.ts @@ -4,27 +4,27 @@ import { isValidCns } from "./is-valid-cns"; describe("isValidCns", () => { describe("should return false", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCns(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCns(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCns()).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCns(true)).toBe(false); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCns({})).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCns([])).toBe(false); }); @@ -82,7 +82,7 @@ describe("isValidCns", () => { }); test("for a definitive CNS as a number", () => { - expect(isValidCns(123456789010000)).toBe(true); + expect(isValidCns(123_456_789_010_000)).toBe(true); }); test("for a definitive CNS with a whitespace mask", () => { diff --git a/src/is-valid-cpf/is-valid-cpf.test.ts b/src/is-valid-cpf/is-valid-cpf.test.ts index 08151936..69a6e15d 100644 --- a/src/is-valid-cpf/is-valid-cpf.test.ts +++ b/src/is-valid-cpf/is-valid-cpf.test.ts @@ -17,29 +17,29 @@ describe("isValidCpf", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCpf(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCpf(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCpf()).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCpf(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCpf(false)).toBe(false); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCpf({})).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCpf([])).toBe(false); }); diff --git a/src/is-valid-credit-card/is-valid-credit-card.test.ts b/src/is-valid-credit-card/is-valid-credit-card.test.ts index 2779990b..f0989a7d 100644 --- a/src/is-valid-credit-card/is-valid-credit-card.test.ts +++ b/src/is-valid-credit-card/is-valid-credit-card.test.ts @@ -20,7 +20,7 @@ describe("isValidCreditCard", () => { }); test("for a number input", () => { - expect(isValidCreditCard(4111111111111111)).toBe(true); + expect(isValidCreditCard(4_111_111_111_111_111)).toBe(true); }); test("for a value with a spaced mask", () => { @@ -70,29 +70,29 @@ describe("isValidCreditCard", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCreditCard(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidCreditCard(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidCreditCard()).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCreditCard(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCreditCard(false)).toBe(false); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCreditCard({})).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidCreditCard([])).toBe(false); }); }); diff --git a/src/is-valid-csosn/is-valid-csosn.test.ts b/src/is-valid-csosn/is-valid-csosn.test.ts index 3325ac58..61585d58 100644 --- a/src/is-valid-csosn/is-valid-csosn.test.ts +++ b/src/is-valid-csosn/is-valid-csosn.test.ts @@ -37,7 +37,7 @@ describe("isValidCsosn", () => { it("should return false for undefined", () => { // @ts-expect-error not a string or number - expect(isValidCsosn(undefined)).toBe(false); + expect(isValidCsosn()).toBe(false); }); it("should return false for a non numeric string", () => { diff --git a/src/is-valid-cst/is-valid-cst.test.ts b/src/is-valid-cst/is-valid-cst.test.ts index d7ac9d16..6288cd92 100644 --- a/src/is-valid-cst/is-valid-cst.test.ts +++ b/src/is-valid-cst/is-valid-cst.test.ts @@ -65,6 +65,11 @@ describe("isValidCst", () => { expect(isValidCst("00", { tax: "iss" })).toBe(false); }); + it("should return false for an unknown tax even when the code is a valid pis/cofins code", () => { + // @ts-expect-error not a valid tax + expect(isValidCst("07", { tax: "iss" })).toBe(false); + }); + describe("without options (tax omitted)", () => { it("should return true when the code is a valid icms combination", () => { expect(isValidCst("110")).toBe(true); @@ -83,7 +88,7 @@ describe("isValidCst", () => { }); it("should return true when options is undefined", () => { - expect(isValidCst("110", undefined)).toBe(true); + expect(isValidCst("110")).toBe(true); }); it("should return true when options.tax is undefined", () => { diff --git a/src/is-valid-cst/is-valid-cst.ts b/src/is-valid-cst/is-valid-cst.ts index be1eb9d8..f8a163b7 100644 --- a/src/is-valid-cst/is-valid-cst.ts +++ b/src/is-valid-cst/is-valid-cst.ts @@ -17,21 +17,15 @@ export type IsValidCstOptions = { const isValidIcmsCst = (digits: string): boolean => digits.charAt(0) <= "8" && (ICMS_CST_CODES as readonly string[]).includes(digits.slice(1)); -const isValidForTax = ( - digits: string, - tax: "icms" | "ipi" | "pis" | "cofins" | undefined, -): boolean => { - switch (tax) { - case "icms": - return isValidIcmsCst(digits); - case "ipi": - return (IPI_CST_CODES as readonly string[]).includes(digits); - case "pis": - case "cofins": - return (PIS_COFINS_CST_CODES as readonly string[]).includes(digits); - default: - return false; +const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): boolean => { + if (tax === "icms") return isValidIcmsCst(digits); + if (tax === "ipi") return (IPI_CST_CODES as readonly string[]).includes(digits); + + if (tax === "pis" || tax === "cofins") { + return (PIS_COFINS_CST_CODES as readonly string[]).includes(digits); } + + return false; }; /** diff --git a/src/is-valid-email/is-valid-email.test.ts b/src/is-valid-email/is-valid-email.test.ts index 33aa6606..2ccbec55 100644 --- a/src/is-valid-email/is-valid-email.test.ts +++ b/src/is-valid-email/is-valid-email.test.ts @@ -8,13 +8,13 @@ describe("isValidEmail", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidEmail(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidEmail(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidEmail()).toBe(false); }); test("when it is missing @", () => { diff --git a/src/is-valid-iban/is-valid-iban.test.ts b/src/is-valid-iban/is-valid-iban.test.ts index c4f8e0d7..d9a0bec6 100644 --- a/src/is-valid-iban/is-valid-iban.test.ts +++ b/src/is-valid-iban/is-valid-iban.test.ts @@ -66,34 +66,34 @@ describe("isValidIban", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIban(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidIban(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidIban()).toBe(false); }); test("when it is a number", () => { - // @ts-expect-error - expect(isValidIban(1500000000000)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidIban(1_500_000_000_000)).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIban(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIban(false)).toBe(false); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIban({})).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIban([])).toBe(false); }); }); diff --git a/src/is-valid-ie/is-valid-ie.test.ts b/src/is-valid-ie/is-valid-ie.test.ts index 820a861a..2c6bdaae 100644 --- a/src/is-valid-ie/is-valid-ie.test.ts +++ b/src/is-valid-ie/is-valid-ie.test.ts @@ -757,29 +757,29 @@ describe("isValidIe", () => { describe("state code lookup", () => { test("should not resolve properties from the prototype chain", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe("constructor", "110042490114")).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe("toString", "110042490114")).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe("__proto__", "110042490114")).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe("valueOf", "110042490114")).toBe(false); }); test("should accept lowercase state codes", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe("sp", "110042490114")).toBe(true); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe("go", "109161793")).toBe(true); }); test("should return false for missing arguments", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe(null, "110042490114")).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe(1, "110042490114")).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe("SP", null)).toBe(false); }); @@ -788,7 +788,7 @@ describe("isValidIe", () => { }); test("should return false when the IE is not a string, even though its digits alone would form a valid checksum", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidIe("RJ", 62_545_372)).toBe(false); }); diff --git a/src/is-valid-ie/is-valid-ie.ts b/src/is-valid-ie/is-valid-ie.ts index 0891fd62..e1713b84 100644 --- a/src/is-valid-ie/is-valid-ie.ts +++ b/src/is-valid-ie/is-valid-ie.ts @@ -23,7 +23,7 @@ const checkLength = (ie: string, length: number | number[]): boolean => { }; const startsWithAny = (ie: string, prefixes: readonly string[]): boolean => - prefixes.some((prefix) => ie.substring(0, prefix.length) === prefix); + prefixes.some((prefix) => ie.slice(0, prefix.length) === prefix); const startsWith = (ie: string, prefix: string): boolean => startsWithAny(ie, [prefix]); @@ -61,7 +61,7 @@ const validateMod11Ie = (ie: string, prefixes?: readonly string[]): boolean => { if (!checkLength(ie, 9)) return false; if (prefixes && !startsWithAny(ie, prefixes)) return false; - const body = ie.substring(0, 8); + const body = ie.slice(0, 8); const sum = calcWeightedSum({ source: body, length: body.length, startWeight: body.length + 1 }); const dig = calcMod11CheckDigit(sum); @@ -86,7 +86,7 @@ const validateAC: IeValidator = (ie: string) => { if (!checkLength(ie, 13)) return false; if (!startsWith(ie, "01")) return false; - const body = ie.substring(0, 11); + const body = ie.slice(0, 11); const firstDig = calcDFDigit(body); const secondDig = calcDFDigit(body + firstDig); @@ -127,7 +127,7 @@ const validateAP: IeValidator = (ie: string) => { const length = ie.length; const position = length - 1; let weight = length; - const body = ie.substring(0, position); + const body = ie.slice(0, position); const bodyInt = Number.parseInt(body, 10); let p = 0; let d = 0; @@ -166,10 +166,10 @@ const validateBA: IeValidator = (ie: string) => { if (!checkLength(ie, [8, 9])) return false; const pos = ie.length === 9 ? 1 : 0; - const charAt = Number.parseInt(ie.substring(pos, pos + 1), 10); + const charAt = Number.parseInt(ie.slice(pos, pos + 1), 10); const mod = BA_MOD_10_DIGITS.includes(charAt) ? 10 : 11; - const body = ie.substring(0, ie.length - 2); + const body = ie.slice(0, ie.length - 2); const firstSum = calcWeightedSum({ source: ie, length: body.length, @@ -186,8 +186,8 @@ const validateBA: IeValidator = (ie: string) => { const firstDig = calcMod11CheckDigit(secondSum, mod); return ( - Number.parseInt(ie.charAt(ie.length - 2), 10) === firstDig && - Number.parseInt(ie.charAt(ie.length - 1), 10) === secondDig + Number.parseInt(ie.slice(-2, -1), 10) === firstDig && + Number.parseInt(ie.slice(-1), 10) === secondDig ); }; @@ -198,7 +198,7 @@ const validateDF: IeValidator = (ie: string) => { if (!startsWith(ie, "07")) return false; const length = ie.length; - const body = ie.substring(0, length - 2); + const body = ie.slice(0, length - 2); const firstDig = calcDFDigit(body); const secondDig = calcDFDigit(body + firstDig); @@ -215,7 +215,7 @@ const validateGO: IeValidator = (ie: string) => { if (!checkLength(ie, 9)) return false; if (!startsWithAny(ie, GO_PREFIXES)) return false; - const body = ie.substring(0, 8); + const body = ie.slice(0, 8); const bodyInt = Number.parseInt(body, 10); const checkDigit = Number.parseInt(ie.charAt(8), 10); @@ -243,8 +243,8 @@ const validateMA: IeValidator = (ie) => validateMod11Ie(ie, MA_PREFIXES); const validateMG: IeValidator = (ie: string) => { if (!checkLength(ie, 13)) return false; - const body = ie.substring(0, 11); - const bodyWithZero = `${body.substring(0, 3)}0${body.substring(3)}`; + const body = ie.slice(0, 11); + const bodyWithZero = `${body.slice(0, 3)}0${body.slice(3)}`; let concat = ""; for (let i = 0; i < bodyWithZero.length; i++) { @@ -288,7 +288,7 @@ const validateMG: IeValidator = (ie: string) => { const validateMT: IeValidator = (ie: string) => { if (!checkLength(ie, 11)) return false; - const body = ie.substring(0, 10); + const body = ie.slice(0, 10); const sum = calcWeightedSum({ source: ie, length: body.length, startWeight: 3, wrapTo: 9 }); const dig = calcMod11CheckDigit(sum); @@ -304,7 +304,7 @@ const validatePB: IeValidator = (ie) => validateMod11Ie(ie); const validatePE: IeValidator = (ie: string) => { if (!checkLength(ie, 9)) return false; - const body = ie.substring(0, 7); + const body = ie.slice(0, 7); const firstSum = calcWeightedSum({ source: ie, length: body.length, @@ -331,7 +331,7 @@ const validatePI: IeValidator = (ie) => validateMod11Ie(ie); const validatePR: IeValidator = (ie: string) => { if (!checkLength(ie, 10)) return false; - const body = ie.substring(0, 8); + const body = ie.slice(0, 8); const firstSum = calcWeightedSum({ source: ie, length: body.length, @@ -358,7 +358,7 @@ const validatePR: IeValidator = (ie: string) => { const validateRJ: IeValidator = (ie: string) => { if (!checkLength(ie, 8)) return false; - const body = ie.substring(0, 7); + const body = ie.slice(0, 7); const sum = calcWeightedSum({ source: ie, length: body.length, startWeight: 2, wrapTo: 7 }); const dig = calcMod11CheckDigit(sum); @@ -371,7 +371,7 @@ const validateRN: IeValidator = (ie: string) => { const length = ie.length; const position = length - 1; - const body = ie.substring(0, position); + const body = ie.slice(0, position); const sum = calcWeightedSum({ source: ie, length: body.length, startWeight: length }); const dig = calcMod11CheckDigit(sum); @@ -383,7 +383,7 @@ const validateRO: IeValidator = (ie: string) => { const length = ie.length; const position = length - 1; - const body = ie.substring(0, position); + const body = ie.slice(0, position); const sum = calcWeightedSum({ source: ie, length: body.length, startWeight: 6, wrapTo: 9 }); const rest = sum % 11; @@ -418,7 +418,7 @@ const validateRR: IeValidator = (ie: string) => { const validateRS: IeValidator = (ie: string) => { if (!checkLength(ie, 10)) return false; - const body = ie.substring(0, 9); + const body = ie.slice(0, 9); const sum = calcWeightedSum({ source: ie, length: body.length, startWeight: 2, wrapTo: 9 }); const dig = calcMod11CheckDigit(sum); @@ -441,7 +441,7 @@ const calcSPDigit = (body: string, weights: readonly number[]): number => { const validateSP: IeValidator = (ie: string) => { if (SP_RURAL_PATTERN.test(ie)) { - const body = ie.substring(1, 9); + const body = ie.slice(1, 9); const dig = calcSPDigit(body, SP_FIRST_WEIGHTS); return Number.parseInt(ie.charAt(9), 10) === dig; @@ -449,8 +449,8 @@ const validateSP: IeValidator = (ie: string) => { if (!SP_COMPANY_PATTERN.test(ie)) return false; - const firstDig = calcSPDigit(ie.substring(0, 8), SP_FIRST_WEIGHTS); - const secondDig = calcSPDigit(ie.substring(0, 11), SP_SECOND_WEIGHTS); + const firstDig = calcSPDigit(ie.slice(0, 8), SP_FIRST_WEIGHTS); + const secondDig = calcSPDigit(ie.slice(0, 11), SP_SECOND_WEIGHTS); return ( Number.parseInt(ie.charAt(8), 10) === firstDig && @@ -463,9 +463,9 @@ const validateTO: IeValidator = (ie: string) => { const isLegacy = ie.length === 11; - if (isLegacy && !TO_TYPES.includes(ie.substring(2, 4))) return false; + if (isLegacy && !TO_TYPES.includes(ie.slice(2, 4))) return false; - const body = isLegacy ? ie.substring(0, 2) + ie.substring(4, 10) : ie.substring(0, 8); + const body = isLegacy ? ie.slice(0, 2) + ie.slice(4, 10) : ie.slice(0, 8); const position = isLegacy ? 10 : 8; const sum = calcWeightedSum({ source: body, length: body.length, startWeight: 9 }); const dig = calcMod11CheckDigit(sum); diff --git a/src/is-valid-landline-phone/is-valid-landline-phone.test.ts b/src/is-valid-landline-phone/is-valid-landline-phone.test.ts index 730b2d4d..c0ecdc18 100644 --- a/src/is-valid-landline-phone/is-valid-landline-phone.test.ts +++ b/src/is-valid-landline-phone/is-valid-landline-phone.test.ts @@ -16,12 +16,12 @@ describe("isValidLandlinePhone", () => { }); test("when it is null, undefined or a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidLandlinePhone(null)).toBe(false); - // @ts-expect-error - expect(isValidLandlinePhone(undefined)).toBe(false); - // @ts-expect-error - expect(isValidLandlinePhone(1130000000)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidLandlinePhone()).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidLandlinePhone(1_130_000_000)).toBe(false); }); test("when the country code leaves an invalid number", () => { diff --git a/src/is-valid-legal-nature/is-valid-legal-nature.test.ts b/src/is-valid-legal-nature/is-valid-legal-nature.test.ts index d92d19e8..240093ba 100644 --- a/src/is-valid-legal-nature/is-valid-legal-nature.test.ts +++ b/src/is-valid-legal-nature/is-valid-legal-nature.test.ts @@ -41,4 +41,10 @@ describe("isValidLegalNature", () => { expect(isValidLegalNature(" 206-2 ")).toBe(true); expect(isValidLegalNature("206.2")).toBe(true); }); + + it("should return false for names inherited from Object.prototype", () => { + expect(isValidLegalNature("constructor")).toBe(false); + expect(isValidLegalNature("toString")).toBe(false); + expect(isValidLegalNature("__proto__")).toBe(false); + }); }); diff --git a/src/is-valid-legal-nature/is-valid-legal-nature.ts b/src/is-valid-legal-nature/is-valid-legal-nature.ts index 94cf6917..7078442a 100644 --- a/src/is-valid-legal-nature/is-valid-legal-nature.ts +++ b/src/is-valid-legal-nature/is-valid-legal-nature.ts @@ -25,5 +25,5 @@ export const isValidLegalNature = (code: string): boolean => { const normalized = code.replace(MASK_REGEX, ""); - return normalized in LEGAL_NATURE; + return Object.hasOwn(LEGAL_NATURE, normalized); }; diff --git a/src/is-valid-license-plate/is-valid-license-plate.test.ts b/src/is-valid-license-plate/is-valid-license-plate.test.ts index 59a249aa..95f0d921 100644 --- a/src/is-valid-license-plate/is-valid-license-plate.test.ts +++ b/src/is-valid-license-plate/is-valid-license-plate.test.ts @@ -8,29 +8,29 @@ describe("isValidLicensePlate", () => { }); it("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidLicensePlate(null)).toBe(false); }); it("when it is undefined", () => { - // @ts-expect-error - expect(isValidLicensePlate(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidLicensePlate()).toBe(false); }); it("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidLicensePlate(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidLicensePlate(false)).toBe(false); }); it("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidLicensePlate({})).toBe(false); }); it("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidLicensePlate([])).toBe(false); }); diff --git a/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts b/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts index 86dfd255..7d2dd0f2 100644 --- a/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts +++ b/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts @@ -16,12 +16,12 @@ describe("isValidMobilePhone", () => { }); test("when it is null, undefined or a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidMobilePhone(null)).toBe(false); - // @ts-expect-error - expect(isValidMobilePhone(undefined)).toBe(false); - // @ts-expect-error - expect(isValidMobilePhone(11987654321)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidMobilePhone()).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidMobilePhone(11_987_654_321)).toBe(false); }); test("when the country code leaves an invalid number", () => { diff --git a/src/is-valid-mobile-phone/is-valid-mobile-phone.ts b/src/is-valid-mobile-phone/is-valid-mobile-phone.ts index 513a09aa..70754423 100644 --- a/src/is-valid-mobile-phone/is-valid-mobile-phone.ts +++ b/src/is-valid-mobile-phone/is-valid-mobile-phone.ts @@ -4,7 +4,7 @@ import { normalizePhone } from "../_internals/normalize-phone/normalize-phone"; import type { PhoneVersion } from "../is-valid-phone/is-valid-phone"; import { MOBILE_VALID_FIRST_NUMBERS_V1, MOBILE_VALID_FIRST_NUMBERS_V2 } from "./constants"; -export type { PhoneVersion }; +export type { PhoneVersion } from "../is-valid-phone/is-valid-phone"; export type IsValidMobilePhoneOptions = { /** Numbering rule to enforce: `1` the pre-2016 8 digit rule, `2` the 9 digit one (default: `2`). */ diff --git a/src/is-valid-ncm/is-valid-ncm.test.ts b/src/is-valid-ncm/is-valid-ncm.test.ts index e343ae4e..1ccaa17e 100644 --- a/src/is-valid-ncm/is-valid-ncm.test.ts +++ b/src/is-valid-ncm/is-valid-ncm.test.ts @@ -11,7 +11,7 @@ describe("isValidNcm", () => { }); it("should validate an NCM code given as a number", () => { - expect(isValidNcm(22030000)).toBe(true); + expect(isValidNcm(22_030_000)).toBe(true); }); it("should validate a leading zero NCM code (cavalos reprodutores de raça pura)", () => { @@ -20,7 +20,7 @@ describe("isValidNcm", () => { }); it("should return false for a number that lost a leading zero (1012100 is not 01012100)", () => { - expect(isValidNcm(1012100)).toBe(false); + expect(isValidNcm(1_012_100)).toBe(false); }); it("should validate an NCM code with surrounding whitespace", () => { @@ -44,7 +44,7 @@ describe("isValidNcm", () => { // @ts-expect-error not a string or number expect(isValidNcm(null)).toBe(false); // @ts-expect-error not a string or number - expect(isValidNcm(undefined)).toBe(false); + expect(isValidNcm()).toBe(false); }); it("should return false for whitespace only", () => { diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts index 8a5f91f6..c8389f53 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts @@ -50,29 +50,29 @@ describe("isValidNfeKey", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidNfeKey(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidNfeKey(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidNfeKey()).toBe(false); }); test("when it is a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidNfeKey(123)).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidNfeKey(true)).toBe(false); }); test("when it is an object or an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidNfeKey({})).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidNfeKey([])).toBe(false); }); @@ -121,7 +121,7 @@ describe("isValidNfeKey", () => { }); describe("with every field valid except one and the check digit recalculated for it", () => { - const CASES: Array<{ name: string; key: string; expected: boolean }> = [ + const CASES: { name: string; key: string; expected: boolean }[] = [ { name: "an unmapped cUF (99)", key: "99200600000000000000550010000000011000000005", diff --git a/src/is-valid-passport/is-valid-passport.test.ts b/src/is-valid-passport/is-valid-passport.test.ts index 3bad9ef9..77579352 100644 --- a/src/is-valid-passport/is-valid-passport.test.ts +++ b/src/is-valid-passport/is-valid-passport.test.ts @@ -8,17 +8,17 @@ describe("isValidPassport", () => { }); test("when passport is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPassport(null)).toBe(false); }); test("when passport is undefined", () => { - // @ts-expect-error - expect(isValidPassport(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidPassport()).toBe(false); }); test("when passport is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPassport({})).toBe(false); }); diff --git a/src/is-valid-passport/is-valid-passport.ts b/src/is-valid-passport/is-valid-passport.ts index fc9039e0..5458659e 100644 --- a/src/is-valid-passport/is-valid-passport.ts +++ b/src/is-valid-passport/is-valid-passport.ts @@ -11,8 +11,8 @@ import { PASSPORT_REGEX } from "./constants"; * This function does not verify if the input is a real passport number, * as there are no checksums for the Brazilian passport. * - * @param passport - The string containing the passport number to be checked. - * @returns True if the passport number is valid (2 letters followed by 6 digits). + * @param {string|number} passport - The string containing the passport number to be checked. + * @returns {boolean} True if the passport number is valid (2 letters followed by 6 digits). * * @example * isValidPassport("AB123456") // true diff --git a/src/is-valid-phone/is-valid-phone.test.ts b/src/is-valid-phone/is-valid-phone.test.ts index 6b0c7b88..85365736 100644 --- a/src/is-valid-phone/is-valid-phone.test.ts +++ b/src/is-valid-phone/is-valid-phone.test.ts @@ -16,15 +16,15 @@ describe("isValidPhone", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPhone(null)).toBe(false); }); test("when it is undefined or a number", () => { - // @ts-expect-error - expect(isValidPhone(undefined)).toBe(false); - // @ts-expect-error - expect(isValidPhone(11987654321)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidPhone()).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidPhone(11_987_654_321)).toBe(false); }); test("when length is invalid", () => { diff --git a/src/is-valid-pis/is-valid-pis.test.ts b/src/is-valid-pis/is-valid-pis.test.ts index f53863e3..a9a5a771 100644 --- a/src/is-valid-pis/is-valid-pis.test.ts +++ b/src/is-valid-pis/is-valid-pis.test.ts @@ -16,35 +16,35 @@ describe("isValidPis", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPis(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidPis(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidPis()).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPis(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPis(false)).toBe(false); }); test("when is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPis({})).toBe(false); }); test("when is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPis([])).toBe(false); }); test("when it is a non-string that stringifies to a valid PIS", () => { // @ts-expect-error not a string - expect(isValidPis([12056412847])).toBe(false); + expect(isValidPis([12_056_412_847])).toBe(false); }); test("when it sanitizes to more digits than the PIS length, even if the first 11 match a valid PIS", () => { diff --git a/src/is-valid-pis/is-valid-pis.ts b/src/is-valid-pis/is-valid-pis.ts index 5e72b61b..428b1dd5 100644 --- a/src/is-valid-pis/is-valid-pis.ts +++ b/src/is-valid-pis/is-valid-pis.ts @@ -33,7 +33,7 @@ export const isValidPis = (pis: string): boolean => { if (RESERVED_NUMBERS.includes(digits)) return false; - const base = digits.substring(0, PIS_LENGTH - 1); + const base = digits.slice(0, PIS_LENGTH - 1); const checkDigit = digits.charCodeAt(PIS_LENGTH - 1) - 48; const weightedChecksum = generateChecksum({ base, weight: PIS_WEIGHTS }); diff --git a/src/is-valid-pix-key/is-valid-pix-key.test.ts b/src/is-valid-pix-key/is-valid-pix-key.test.ts index 8c1122bb..81decd34 100644 --- a/src/is-valid-pix-key/is-valid-pix-key.test.ts +++ b/src/is-valid-pix-key/is-valid-pix-key.test.ts @@ -11,26 +11,26 @@ describe("isValidPixKey", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixKey(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidPixKey(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidPixKey()).toBe(false); }); test("when it is a number", () => { - // @ts-expect-error - expect(isValidPixKey(12345678909)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidPixKey(12_345_678_909)).toBe(false); }); test("when it is a boolean, an object or an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixKey(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixKey({})).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixKey([])).toBe(false); }); @@ -89,9 +89,9 @@ describe("isValidPixKey", () => { test("accepting every kind when the option is absent or not a list", () => { expect(isValidPixKey("123.456.789-09", {})).toBe(true); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixKey("123.456.789-09", { accept: "cpf" })).toBe(true); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixKey("123.456.789-09", null)).toBe(true); }); }); diff --git a/src/is-valid-pix-payload/is-valid-pix-payload.test.ts b/src/is-valid-pix-payload/is-valid-pix-payload.test.ts index 8bf7d273..0c4d3a59 100644 --- a/src/is-valid-pix-payload/is-valid-pix-payload.test.ts +++ b/src/is-valid-pix-payload/is-valid-pix-payload.test.ts @@ -62,26 +62,26 @@ describe("isValidPixPayload", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixPayload(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidPixPayload(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidPixPayload()).toBe(false); }); test("when it is a number", () => { - // @ts-expect-error - expect(isValidPixPayload(20250101)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidPixPayload(20_250_101)).toBe(false); }); test("when it is a boolean, an object or an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixPayload(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixPayload({})).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidPixPayload([])).toBe(false); }); diff --git a/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts b/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts index 4a58df48..5fd6f876 100644 --- a/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts +++ b/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts @@ -9,13 +9,13 @@ describe("isValidProcessoJuridico", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidProcessoJuridico(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidProcessoJuridico(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidProcessoJuridico()).toBe(false); }); test(`when length is less than ${PROCESSO_JURIDICO_LENGTH}`, () => { diff --git a/src/is-valid-processo-juridico/is-valid-processo-juridico.ts b/src/is-valid-processo-juridico/is-valid-processo-juridico.ts index 42059151..87bf6752 100644 --- a/src/is-valid-processo-juridico/is-valid-processo-juridico.ts +++ b/src/is-valid-processo-juridico/is-valid-processo-juridico.ts @@ -9,13 +9,13 @@ import { const verifyCheckDigit = (value: string): boolean => { const verificationDigits = Number.parseInt( - value.substring(CHECK_DIGIT_START_POSITION, CHECK_DIGIT_START_POSITION + CHECK_DIGIT_LENGTH), + value.slice(CHECK_DIGIT_START_POSITION, CHECK_DIGIT_START_POSITION + CHECK_DIGIT_LENGTH), 10, ); const withoutCheck = - value.substring(0, CHECK_DIGIT_START_POSITION) + - value.substring(CHECK_DIGIT_START_POSITION + CHECK_DIGIT_LENGTH); + value.slice(0, CHECK_DIGIT_START_POSITION) + + value.slice(CHECK_DIGIT_START_POSITION + CHECK_DIGIT_LENGTH); let digits1to11 = 0; for (let i = 0; i < 11; i++) { diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts index ebfba1ca..f3d80fa5 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts @@ -4,7 +4,7 @@ import { isValidRegistroProfissional } from "./is-valid-registro-profissional"; describe("isValidRegistroProfissional", () => { describe("should return false", () => { test("when value is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidRegistroProfissional(null, { council: "OAB" })).toBe(false); }); @@ -13,12 +13,12 @@ describe("isValidRegistroProfissional", () => { }); test("when options is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidRegistroProfissional("123456/SP", null)).toBe(false); }); test("when the council is not supported (e.g. CREA)", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidRegistroProfissional("1234567890", { council: "CREA" })).toBe(false); }); diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts index b00e3367..255f9321 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts @@ -75,9 +75,9 @@ export const isValidRegistroProfissional = ( if (typeof options !== "object" || options === null) return false; - const regex = REGEX_BY_COUNCIL[options.council]; + if (!Object.hasOwn(REGEX_BY_COUNCIL, options.council)) return false; - if (!regex) return false; + const regex = REGEX_BY_COUNCIL[options.council]; const match = regex.exec(sanitizeToAlphanumeric(value)); @@ -85,7 +85,7 @@ export const isValidRegistroProfissional = ( const { uf } = match.groups; - if (!uf) return true; + if (uf === undefined) return true; if (!isKnownStateCode(uf)) return false; diff --git a/src/is-valid-renavam/is-valid-renavam.test.ts b/src/is-valid-renavam/is-valid-renavam.test.ts index c69f8078..3d41fb25 100644 --- a/src/is-valid-renavam/is-valid-renavam.test.ts +++ b/src/is-valid-renavam/is-valid-renavam.test.ts @@ -8,29 +8,29 @@ describe("isValidRenavam", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidRenavam(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidRenavam(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidRenavam()).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidRenavam(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidRenavam(false)).toBe(false); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidRenavam({})).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidRenavam([])).toBe(false); }); @@ -73,7 +73,7 @@ describe("isValidRenavam", () => { }); test("when is a RENAVAM valid as number", () => { - expect(isValidRenavam(639884962)).toBe(true); + expect(isValidRenavam(639_884_962)).toBe(true); }); test("when is a RENAVAM valid with mixed characters that sanitize to a valid RENAVAM", () => { diff --git a/src/is-valid-renavam/is-valid-renavam.ts b/src/is-valid-renavam/is-valid-renavam.ts index 8a941b47..35f48c3e 100644 --- a/src/is-valid-renavam/is-valid-renavam.ts +++ b/src/is-valid-renavam/is-valid-renavam.ts @@ -27,7 +27,7 @@ const padLeft = (input: string, padLength: number): string => * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9503compilado.htm */ export const isValidRenavam = (renavam: string | number): boolean => { - if (!renavam) return false; + if (typeof renavam !== "string" && typeof renavam !== "number") return false; const digits = sanitizeToDigits(renavam); @@ -35,14 +35,18 @@ export const isValidRenavam = (renavam: string | number): boolean => { const paddedDigits = padLeft(digits, RENAVAM_LENGTH); - const renavamWithoutDigit = paddedDigits.substring(0, 10); + const renavamWithoutDigit = paddedDigits.slice(0, 10); - const reversedRenavam = renavamWithoutDigit.split("").reverse().join(""); + let reversedRenavam = ""; + + for (const char of renavamWithoutDigit) { + reversedRenavam = char + reversedRenavam; + } let sum = 0; let multiplier = 2; - for (let i = 0; i < 10; i++) { - const digit = Number.parseInt(reversedRenavam[i], 10); + for (const char of reversedRenavam) { + const digit = Number.parseInt(char, 10); sum += digit * multiplier; multiplier = multiplier >= 9 ? 2 : multiplier + 1; @@ -52,7 +56,7 @@ export const isValidRenavam = (renavam: string | number): boolean => { const expectedDigit = mod11 <= 1 ? 0 : 11 - mod11; - const actualDigit = Number.parseInt(paddedDigits[10], 10); + const actualDigit = Number.parseInt(paddedDigits.charAt(10), 10); return expectedDigit === actualDigit; }; diff --git a/src/is-valid-service-phone/is-valid-service-phone.test.ts b/src/is-valid-service-phone/is-valid-service-phone.test.ts index 1c57b343..1d3bb507 100644 --- a/src/is-valid-service-phone/is-valid-service-phone.test.ts +++ b/src/is-valid-service-phone/is-valid-service-phone.test.ts @@ -8,12 +8,12 @@ describe("isValidServicePhone", () => { }); test("when it is null, undefined or a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidServicePhone(null)).toBe(false); - // @ts-expect-error - expect(isValidServicePhone(undefined)).toBe(false); - // @ts-expect-error - expect(isValidServicePhone(8001234567)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidServicePhone()).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidServicePhone(8_001_234_567)).toBe(false); }); test("when it is a geographic number", () => { diff --git a/src/is-valid-vin/is-valid-vin.test.ts b/src/is-valid-vin/is-valid-vin.test.ts index a12a2765..d84f6774 100644 --- a/src/is-valid-vin/is-valid-vin.test.ts +++ b/src/is-valid-vin/is-valid-vin.test.ts @@ -49,6 +49,10 @@ describe("isValidVin", () => { expect(isValidVin("1HGCM82633A00435")).toBe(false); }); + test("when it has 16 characters whose weighted sum coincidentally matches its own 9th character", () => { + expect(isValidVin("Z92D746W7W5N6SFH")).toBe(false); + }); + test("when it has more than 17 characters", () => { expect(isValidVin("1HGCM82633A0043522")).toBe(false); }); @@ -70,34 +74,34 @@ describe("isValidVin", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidVin(null)).toBe(false); }); test("when it is undefined", () => { - // @ts-expect-error - expect(isValidVin(undefined)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidVin()).toBe(false); }); test("when it is a number", () => { - // @ts-expect-error - expect(isValidVin(12345678901234)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidVin(12_345_678_901_234)).toBe(false); }); test("when it is a boolean", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidVin(true)).toBe(false); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidVin(false)).toBe(false); }); test("when it is an object", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidVin({})).toBe(false); }); test("when it is an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidVin([])).toBe(false); }); }); diff --git a/src/is-valid-vin/is-valid-vin.ts b/src/is-valid-vin/is-valid-vin.ts index 174ef9bf..db1959fb 100644 --- a/src/is-valid-vin/is-valid-vin.ts +++ b/src/is-valid-vin/is-valid-vin.ts @@ -40,9 +40,7 @@ export const isValidVin = (value: string): boolean => { // Stryker disable next-line StringLiteral: generateChecksum strips this to digits, so it's inert. let translitDigits = ""; - for (let i = 0; i < VIN_LENGTH; i++) { - const char = vin[i]; - + for (const char of vin) { if (!(char in VIN_TRANSLITERATION)) return false; translitDigits += VIN_TRANSLITERATION[char]; diff --git a/src/is-valid-voter-id/is-valid-voter-id.test.ts b/src/is-valid-voter-id/is-valid-voter-id.test.ts index 46582e07..bdba32ca 100644 --- a/src/is-valid-voter-id/is-valid-voter-id.test.ts +++ b/src/is-valid-voter-id/is-valid-voter-id.test.ts @@ -59,18 +59,18 @@ describe("isValidVoterId", () => { }); it("should return false for null, undefined, a number or an empty string", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(isValidVoterId(null)).toBe(false); - // @ts-expect-error - expect(isValidVoterId(undefined)).toBe(false); - // @ts-expect-error - expect(isValidVoterId(123456780124)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidVoterId()).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidVoterId(123_456_780_124)).toBe(false); expect(isValidVoterId("")).toBe(false); }); it("should reject a valid voter id passed as a number instead of a string", () => { - // @ts-expect-error - expect(isValidVoterId(102385010671)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidVoterId(102_385_010_671)).toBe(false); }); it("should reject a value whose length is neither 12 nor 13, even when its checksum would otherwise match", () => { diff --git a/src/parse-boleto/parse-boleto.test.ts b/src/parse-boleto/parse-boleto.test.ts index a52fff23..4897059e 100644 --- a/src/parse-boleto/parse-boleto.test.ts +++ b/src/parse-boleto/parse-boleto.test.ts @@ -15,10 +15,10 @@ describe("parseBoleto", () => { }); it("should return an empty string when the value is nullish", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parseBoleto(null)).toBe(""); - // @ts-expect-error - expect(parseBoleto(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(parseBoleto()).toBe(""); }); it("should ignore digits after the boleto length", () => { diff --git a/src/parse-certidao/parse-certidao.test.ts b/src/parse-certidao/parse-certidao.test.ts index 5d57f518..99a5ac17 100644 --- a/src/parse-certidao/parse-certidao.test.ts +++ b/src/parse-certidao/parse-certidao.test.ts @@ -4,13 +4,13 @@ import { parseCertidao } from "./parse-certidao"; describe("parseCertidao", () => { describe("should return null", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parseCertidao(null)).toBeNull(); }); test("when it is undefined", () => { - // @ts-expect-error - expect(parseCertidao(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(parseCertidao()).toBeNull(); }); test("when it is an empty string", () => { diff --git a/src/parse-certidao/parse-certidao.ts b/src/parse-certidao/parse-certidao.ts index 1564c89a..64a09332 100644 --- a/src/parse-certidao/parse-certidao.ts +++ b/src/parse-certidao/parse-certidao.ts @@ -61,14 +61,16 @@ export const parseCertidao = (value: string | number): Certidao | null => { const digits = sanitizeToDigits(value); const typeCode = digits.charCodeAt(14) - 48; - if (typeCode < 1) return null; + const type: CertidaoType | undefined = CERTIDAO_TYPES[typeCode - 1]; + + if (type === undefined) return null; return { registryCns: digits.slice(0, 6), acervo: digits.slice(6, 8), service: digits.slice(8, 10), year: Number(digits.slice(10, 14)), - type: CERTIDAO_TYPES[typeCode - 1], + type, typeCode, book: digits.slice(15, 20), page: digits.slice(20, 23), diff --git a/src/parse-currency/parse-currency.test.ts b/src/parse-currency/parse-currency.test.ts index 4540e126..fb99af25 100644 --- a/src/parse-currency/parse-currency.test.ts +++ b/src/parse-currency/parse-currency.test.ts @@ -22,8 +22,8 @@ describe("parseCurrency", () => { }); test("when parsing large values", () => { - expect(parseCurrency("R$ 10.000,00")).toBe(10000); - expect(parseCurrency("1.000.000,50")).toBe(1000000.5); + expect(parseCurrency("R$ 10.000,00")).toBe(10_000); + expect(parseCurrency("1.000.000,50")).toBe(1_000_000.5); }); }); @@ -33,13 +33,13 @@ describe("parseCurrency", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parseCurrency(null)).toBe(0); }); test("when it is undefined", () => { - // @ts-expect-error - expect(parseCurrency(undefined)).toBe(0); + // @ts-expect-error: intentionally invalid input + expect(parseCurrency()).toBe(0); }); test("should transform a formatted value into a float", () => { @@ -50,9 +50,9 @@ describe("parseCurrency", () => { expect(parseCurrency("R$ 10,01")).toBe(10.01); expect(parseCurrency("R$ 100,01")).toBe(100.01); expect(parseCurrency("R$ 1.000,01")).toBe(1000.01); - expect(parseCurrency("R$ 10.000,01")).toBe(10000.01); - expect(parseCurrency("R$ 100.000,01")).toBe(100000.01); - expect(parseCurrency("R$ 1.000.000,01")).toBe(1000000.01); + expect(parseCurrency("R$ 10.000,01")).toBe(10_000.01); + expect(parseCurrency("R$ 100.000,01")).toBe(100_000.01); + expect(parseCurrency("R$ 1.000.000,01")).toBe(1_000_000.01); }); }); @@ -81,7 +81,7 @@ describe("parseCurrency", () => { test("when there is only a thousands separator", () => { expect(parseCurrency("R$ 1.234")).toBe(1234); - expect(parseCurrency("R$ 1.000.000")).toBe(1000000); + expect(parseCurrency("R$ 1.000.000")).toBe(1_000_000); expect(parseCurrency("1,5")).toBe(1.5); expect(parseCurrency("1.059")).toBe(1059); }); @@ -116,8 +116,8 @@ describe("parseCurrency", () => { test("when using a custom precision", () => { expect(parseCurrency(formatCurrency(1.001, { precision: 3 }), { precision: 3 })).toBe(1.001); - expect(parseCurrency(formatCurrency(1000000.001, { precision: 3 }), { precision: 3 })).toBe( - 1000000.001, + expect(parseCurrency(formatCurrency(1_000_000.001, { precision: 3 }), { precision: 3 })).toBe( + 1_000_000.001, ); }); }); diff --git a/src/parse-iban/parse-iban.test.ts b/src/parse-iban/parse-iban.test.ts index aa442e80..fde05391 100644 --- a/src/parse-iban/parse-iban.test.ts +++ b/src/parse-iban/parse-iban.test.ts @@ -92,18 +92,18 @@ describe("parseIban", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parseIban(null)).toBeNull(); }); test("when it is undefined", () => { - // @ts-expect-error - expect(parseIban(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(parseIban()).toBeNull(); }); test("when it is a number", () => { - // @ts-expect-error - expect(parseIban(150000000000)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(parseIban(150_000_000_000)).toBeNull(); }); }); diff --git a/src/parse-nfe-key/parse-nfe-key.test.ts b/src/parse-nfe-key/parse-nfe-key.test.ts index 0505f89e..ec106b0a 100644 --- a/src/parse-nfe-key/parse-nfe-key.test.ts +++ b/src/parse-nfe-key/parse-nfe-key.test.ts @@ -8,17 +8,17 @@ const KEY_CPF_PADDED = "35170400040364478829550010000000121000123457"; describe("parseNfeKey", () => { describe("should return null", () => { test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parseNfeKey(null)).toBeNull(); }); test("when it is undefined", () => { - // @ts-expect-error - expect(parseNfeKey(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(parseNfeKey()).toBeNull(); }); test("when it is a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parseNfeKey(123)).toBeNull(); }); diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/parse-nfe-key/parse-nfe-key.ts index a3c67efc..e2030de5 100644 --- a/src/parse-nfe-key/parse-nfe-key.ts +++ b/src/parse-nfe-key/parse-nfe-key.ts @@ -66,7 +66,9 @@ export const parseNfeKey = (value: string): NfeKey | null => { const uf = digits.slice(0, 2); - if (!Object.hasOwn(IBGE_UF_CODES, uf)) return null; + const state = IBGE_UF_CODES[uf]; + + if (state === undefined) return null; const month = Number(digits.slice(4, 6)); @@ -88,7 +90,7 @@ export const parseNfeKey = (value: string): NfeKey | null => { if (mod11(digits.slice(0, 43), { variant: "arrecadacao" }) !== checkDigit) return null; return { - state: IBGE_UF_CODES[uf], + state, year: 2000 + Number(digits.slice(2, 4)), month, taxId: digits.slice(6, 20), diff --git a/src/parse-passport/parse-passport.ts b/src/parse-passport/parse-passport.ts index 5a521e89..1eb9affc 100644 --- a/src/parse-passport/parse-passport.ts +++ b/src/parse-passport/parse-passport.ts @@ -4,8 +4,8 @@ import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/s /** * Removes non-alphanumeric characters from a passport number, uppercases it, and caps it to 8 characters. * - * @param passport - The string containing a passport number. - * @returns The normalized passport number. + * @param {string} passport - The string containing a passport number. + * @returns {string} The normalized passport number. * * @example * parsePassport("Ab123456") // "AB123456" diff --git a/src/parse-phone/parse-phone.test.ts b/src/parse-phone/parse-phone.test.ts index 9089720a..296c07b5 100644 --- a/src/parse-phone/parse-phone.test.ts +++ b/src/parse-phone/parse-phone.test.ts @@ -41,13 +41,13 @@ describe("parsePhone", () => { }); it("should return an empty string for nullish values", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePhone(null)).toBe(""); - // @ts-expect-error - expect(parsePhone(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(parsePhone()).toBe(""); }); it("should accept numbers", () => { - expect(parsePhone(11988887777)).toBe("11988887777"); + expect(parsePhone(11_988_887_777)).toBe("11988887777"); }); }); diff --git a/src/parse-pix-key/parse-pix-key.test.ts b/src/parse-pix-key/parse-pix-key.test.ts index 4760f61a..661e62b4 100644 --- a/src/parse-pix-key/parse-pix-key.test.ts +++ b/src/parse-pix-key/parse-pix-key.test.ts @@ -14,26 +14,26 @@ describe("parsePixKey", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePixKey(null)).toBeNull(); }); test("when it is undefined", () => { - // @ts-expect-error - expect(parsePixKey(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(parsePixKey()).toBeNull(); }); test("when it is a number", () => { - // @ts-expect-error - expect(parsePixKey(12345678909)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(parsePixKey(12_345_678_909)).toBeNull(); }); test("when it is a boolean, an object or an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePixKey(true)).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePixKey({})).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePixKey([])).toBeNull(); }); diff --git a/src/parse-pix-payload/parse-pix-payload.test.ts b/src/parse-pix-payload/parse-pix-payload.test.ts index c180875c..39f4ae16 100644 --- a/src/parse-pix-payload/parse-pix-payload.test.ts +++ b/src/parse-pix-payload/parse-pix-payload.test.ts @@ -34,7 +34,7 @@ const buildPayload = (merchantAccountInformation: string, additionalData?: strin tlv("58", "BR") + tlv("59", "Fulano de Tal") + tlv("60", "BRASILIA") + - (additionalData !== undefined ? tlv("62", additionalData) : "") + + (additionalData === undefined ? "" : tlv("62", additionalData)) + "6304"; return withoutCrc + crc16Ccitt(withoutCrc); @@ -69,35 +69,57 @@ const buildPayloadWithoutCountryCode = (): string => { return withoutCrc + crc16Ccitt(withoutCrc); }; +const buildPayloadBody = (merchantCity: string, crcTag: string): string => + tlv("00", "01") + + tlv("26", MERCHANT_ACCOUNT_INFORMATION) + + tlv("52", "0000") + + tlv("53", "986") + + tlv("58", "BR") + + tlv("59", "Fulano de Tal") + + tlv("60", merchantCity) + + crcTag; + const buildPayloadWithCrcTag = (crcTag: string): string => { + const withoutCrc = buildPayloadBody("BRASILIA", crcTag); + + return withoutCrc + crc16Ccitt(withoutCrc); +}; + +const buildPayloadWithAmount = (amount: string): string => { const withoutCrc = tlv("00", "01") + tlv("26", MERCHANT_ACCOUNT_INFORMATION) + tlv("52", "0000") + tlv("53", "986") + + tlv("54", amount) + tlv("58", "BR") + tlv("59", "Fulano de Tal") + tlv("60", "BRASILIA") + - crcTag; + "6304"; return withoutCrc + crc16Ccitt(withoutCrc); }; -const buildPayloadWithAmount = (amount: string): string => { +const buildPayloadWithMerchantName = (merchantName: string): string => { const withoutCrc = tlv("00", "01") + tlv("26", MERCHANT_ACCOUNT_INFORMATION) + tlv("52", "0000") + tlv("53", "986") + - tlv("54", amount) + tlv("58", "BR") + - tlv("59", "Fulano de Tal") + + tlv("59", merchantName) + tlv("60", "BRASILIA") + "6304"; return withoutCrc + crc16Ccitt(withoutCrc); }; +const buildPayloadWithMerchantCity = (merchantCity: string): string => { + const withoutCrc = buildPayloadBody(merchantCity, "6304"); + + return withoutCrc + crc16Ccitt(withoutCrc); +}; + describe("parsePixPayload", () => { describe("should return null", () => { test("when it is an empty or blank string", () => { @@ -106,26 +128,26 @@ describe("parsePixPayload", () => { }); test("when it is null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePixPayload(null)).toBeNull(); }); test("when it is undefined", () => { - // @ts-expect-error - expect(parsePixPayload(undefined)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(parsePixPayload()).toBeNull(); }); test("when it is a number", () => { - // @ts-expect-error - expect(parsePixPayload(20250101)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(parsePixPayload(20_250_101)).toBeNull(); }); test("when it is a boolean, an object or an array", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePixPayload(true)).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePixPayload({})).toBeNull(); - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parsePixPayload([])).toBeNull(); }); @@ -186,7 +208,9 @@ describe("parsePixPayload", () => { }); test("when a merchant account information template is well-formed but carries no GUI, without throwing", () => { - expect(parsePixPayload(buildPayload(tlv("01", "12345678909")))).toBeNull(); + const merchantAccountInformation = tlv("01", "12345678909"); + + expect(parsePixPayload(buildPayload(merchantAccountInformation))).toBeNull(); }); test("when the country code field is entirely absent, without throwing", () => { @@ -200,6 +224,14 @@ describe("parsePixPayload", () => { test("when the transaction amount is longer than 13 characters", () => { expect(parsePixPayload(buildPayloadWithAmount("99999999999.99"))).toBeNull(); }); + + test("when the merchant name is present but empty", () => { + expect(parsePixPayload(buildPayloadWithMerchantName(""))).toBeNull(); + }); + + test("when the merchant city is present but empty", () => { + expect(parsePixPayload(buildPayloadWithMerchantCity(""))).toBeNull(); + }); }); describe("should parse a static payload", () => { @@ -257,7 +289,9 @@ describe("parsePixPayload", () => { }); test("accepting a transaction amount whose length is exactly 13 characters", () => { - expect(parsePixPayload(buildPayloadWithAmount("9999999999.99"))?.amount).toBe(9999999999.99); + expect(parsePixPayload(buildPayloadWithAmount("9999999999.99"))?.amount).toBe( + 9_999_999_999.99, + ); }); test("accepting a transaction amount written as a whole number, with no decimal point", () => { diff --git a/src/parse-pix-payload/parse-pix-payload.ts b/src/parse-pix-payload/parse-pix-payload.ts index 8514ac11..e982acdc 100644 --- a/src/parse-pix-payload/parse-pix-payload.ts +++ b/src/parse-pix-payload/parse-pix-payload.ts @@ -87,6 +87,92 @@ const isValidCrc = (payload: string): boolean => { return crc16Ccitt(payload.slice(0, -PIX_CRC_LENGTH)) === checksum.toUpperCase(); }; +const resolvePointOfInitiation = (fields: TlvFields): string | undefined | null => { + const pointOfInitiation = fields[PIX_POINT_OF_INITIATION_ID]; + + if ( + pointOfInitiation !== undefined && + pointOfInitiation !== PIX_STATIC_POINT_OF_INITIATION && + pointOfInitiation !== PIX_DYNAMIC_POINT_OF_INITIATION + ) { + return null; + } + + return pointOfInitiation; +}; + +const isValidAmount = (amount: string | undefined): boolean => + amount === undefined || + (AMOUNT_REGEX.test(amount) && amount.length <= PIX_TRANSACTION_AMOUNT_MAX_LENGTH); + +type MerchantKeyInfo = { + key?: string | undefined; + url?: string | undefined; + description?: string | undefined; +}; + +const resolveMerchantKeyInfo = (fields: TlvFields): MerchantKeyInfo | null => { + const merchantAccountInformation = findMerchantAccountInformation(fields); + + if (!merchantAccountInformation) return null; + + const key = merchantAccountInformation[PIX_KEY_ID]; + const url = merchantAccountInformation[PIX_URL_ID]; + const description = merchantAccountInformation[PIX_DESCRIPTION_ID]; + + if ((key === undefined) === (url === undefined)) return null; + if (key !== undefined && !key) return null; + if (url !== undefined && !isValidPixUrl(url)) return null; + + return { key, url, description }; +}; + +const resolveTxid = (fields: TlvFields): string | undefined | null => { + const additionalData = fields[PIX_ADDITIONAL_DATA_ID]; + + if (additionalData === undefined) return undefined; + + const objects = parseTlv(additionalData); + + if (!objects) return null; + + return objects[PIX_TXID_ID]; +}; + +type OptionalPixFields = { + key?: string | undefined; + url?: string | undefined; + description?: string | undefined; + amount?: string | undefined; + txid?: string | undefined; + pointOfInitiation?: string | undefined; +}; + +const buildPixPayload = ( + merchantName: string, + merchantCity: string, + optional: OptionalPixFields, +): PixPayload => { + const { key, url, description, amount, txid, pointOfInitiation } = optional; + const pix: PixPayload = { merchantName, merchantCity }; + + if (key !== undefined) pix.key = key; + if (url !== undefined) pix.url = url; + if (description !== undefined) pix.description = description; + + const isDynamic = pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION; + + if (amount !== undefined && !isDynamic) pix.amount = Number(amount); + if (txid !== undefined && txid !== PIX_ABSENT_TXID && !isDynamic) pix.txid = txid; + + if (pointOfInitiation !== undefined) { + pix.pointOfInitiation = + pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION ? "dynamic" : "static"; + } + + return pix; +}; + /** * Parses a Pix BR Code payload, the string behind a Pix QR Code and behind "Pix copia e cola". * @@ -144,15 +230,9 @@ export const parsePixPayload = (value: string): PixPayload | null => { if (fields[PIX_PAYLOAD_FORMAT_INDICATOR_ID] !== PIX_PAYLOAD_FORMAT_INDICATOR) return null; - const pointOfInitiation = fields[PIX_POINT_OF_INITIATION_ID]; + const pointOfInitiation = resolvePointOfInitiation(fields); - if ( - pointOfInitiation !== undefined && - pointOfInitiation !== PIX_STATIC_POINT_OF_INITIATION && - pointOfInitiation !== PIX_DYNAMIC_POINT_OF_INITIATION - ) { - return null; - } + if (pointOfInitiation === null) return null; if (fields[PIX_MERCHANT_CATEGORY_CODE_ID] === undefined) return null; if (fields[PIX_TRANSACTION_CURRENCY_ID] !== PIX_TRANSACTION_CURRENCY) return null; @@ -160,59 +240,28 @@ export const parsePixPayload = (value: string): PixPayload | null => { const merchantName = fields[PIX_MERCHANT_NAME_ID]; - if (!merchantName) return null; + if (merchantName === undefined || merchantName === "") return null; const merchantCity = fields[PIX_MERCHANT_CITY_ID]; - if (!merchantCity) return null; + if (merchantCity === undefined || merchantCity === "") return null; const amount = fields[PIX_TRANSACTION_AMOUNT_ID]; - if ( - amount !== undefined && - (!AMOUNT_REGEX.test(amount) || amount.length > PIX_TRANSACTION_AMOUNT_MAX_LENGTH) - ) { - return null; - } - - const merchantAccountInformation = findMerchantAccountInformation(fields); - - if (!merchantAccountInformation) return null; - - const key = merchantAccountInformation[PIX_KEY_ID]; - const url = merchantAccountInformation[PIX_URL_ID]; - const description = merchantAccountInformation[PIX_DESCRIPTION_ID]; - - if ((key === undefined) === (url === undefined)) return null; - if (key !== undefined && !key) return null; - if (url !== undefined && !isValidPixUrl(url)) return null; + if (!isValidAmount(amount)) return null; - const additionalData = fields[PIX_ADDITIONAL_DATA_ID]; + const merchantKeyInfo = resolveMerchantKeyInfo(fields); - let txid: string | undefined; + if (!merchantKeyInfo) return null; - if (additionalData !== undefined) { - const objects = parseTlv(additionalData); + const txid = resolveTxid(fields); - if (!objects) return null; + if (txid === null) return null; - txid = objects[PIX_TXID_ID]; - } - - const pix: PixPayload = { merchantName, merchantCity }; - - if (key !== undefined) pix.key = key; - if (url !== undefined) pix.url = url; - if (description !== undefined) pix.description = description; - const isDynamic = pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION; - - if (amount !== undefined && !isDynamic) pix.amount = Number(amount); - if (txid !== undefined && txid !== PIX_ABSENT_TXID && !isDynamic) pix.txid = txid; - - if (pointOfInitiation !== undefined) { - pix.pointOfInitiation = - pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION ? "dynamic" : "static"; - } - - return pix; + return buildPixPayload(merchantName, merchantCity, { + ...merchantKeyInfo, + amount, + txid, + pointOfInitiation, + }); }; diff --git a/src/parse-voter-id/parse-voter-id.test.ts b/src/parse-voter-id/parse-voter-id.test.ts index 7710e9e7..4c213ff8 100644 --- a/src/parse-voter-id/parse-voter-id.test.ts +++ b/src/parse-voter-id/parse-voter-id.test.ts @@ -23,10 +23,10 @@ describe("parseVoterId", () => { }); it("should return an empty string for null or undefined", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(parseVoterId(null)).toBe(""); - // @ts-expect-error - expect(parseVoterId(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(parseVoterId()).toBe(""); }); it("should ignore digits after the 12-digit length when the 9th/10th digits are not SP/MG, even with extra digits", () => { diff --git a/src/remove-accents/remove-accents.test.ts b/src/remove-accents/remove-accents.test.ts index 105144ca..e937cfbe 100644 --- a/src/remove-accents/remove-accents.test.ts +++ b/src/remove-accents/remove-accents.test.ts @@ -44,17 +44,17 @@ describe("removeAccents", () => { }); it("should return an empty string when given null", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(removeAccents(null)).toBe(""); }); it("should return an empty string when given undefined", () => { - // @ts-expect-error - expect(removeAccents(undefined)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(removeAccents()).toBe(""); }); it("should return an empty string when given a number", () => { - // @ts-expect-error + // @ts-expect-error: intentionally invalid input expect(removeAccents(123)).toBe(""); }); }); From d11b8e86c72c8169800e3b28f66b3f110e356439 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 12 Sep 2026 08:17:33 -0300 Subject: [PATCH 2/2] ci: tighten oxlint and TypeScript strictness oxlint now runs the correctness, suspicious, perf and pedantic categories as errors with the import, jsdoc, promise, unicorn, typescript and vitest plugins, plus a curated set of restriction and style rules; test files relax what only makes sense for production code. tsconfig adds noImplicitOverride, noUnusedLocals, noUnusedParameters and noPropertyAccessFromIndexSignature. noUncheckedIndexedAccess and exactOptionalPropertyTypes stay off: the lookup tables are indexed by already-validated digits, so those flags only add unreachable fallbacks that show up as missing coverage and equivalent mutants. CONTRIBUTING documents the rule set. --- CONTRIBUTING.md | 24 ++++++++++++++ tsconfig.json | 7 ++-- vite.config.ts | 85 +++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 104 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e407add3..5cee6020 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,6 +124,30 @@ request: the report is still posted, but the check no longer fails. Run `node sc or `node scripts/tree-shaking.ts --json before.json` before a change and `node scripts/tree-shaking.ts --compare before.json` after it to preview the same diff. +## Lint and type strictness + +`npm check` runs oxlint through Vite+ with the `correctness`, `suspicious`, `perf` and `pedantic` +categories as errors, the `import`, `jsdoc` and `promise` plugins, and a curated set of +`restriction`/`style` rules on top (see `lint.rules` in `vite.config.ts`): explicit return types +on every function, no `console` outside `scripts/`, no `forEach`, no parameter reassignment, no +non-null assertions, no unsafe type assertions, JSDoc `@param`/`@returns` with types on exported +functions, `type` over `interface`, `T[]` over `Array`, and no default exports outside the +config files. Test files relax the rules that only make sense for production code (return types, +JSDoc, the `unsafe-*` family, since the multi-runtime `expect` shim is untyped) and every +`@ts-expect-error` must carry a description. + +`tsconfig.json` is `strict` plus `noImplicitOverride`, `noUnusedLocals`, `noUnusedParameters` and +`noPropertyAccessFromIndexSignature`. `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` +stay off on purpose: the lookup tables are indexed by digits the code has already validated, so +those flags only add unreachable fallbacks, and every unreachable branch shows up as missing +coverage and as an equivalent mutant. Fix a type error with a real check that returns the same +value the code returned before, never with `!` or `as`. + +Two pedantic rules stay off on purpose: `require-unicode-regexp` (the `u` flag changes what a +few escapes mean) and `prefer-code-point`/`prefer-number-coercion` (the digit arithmetic on +`charCodeAt` and `parseInt` is deliberate, and `codePointAt` would add a nullable branch to every +check-digit loop). + ## Code quality gates Three extra gates run in CI next to lint, types and coverage; run them locally before opening a diff --git a/tsconfig.json b/tsconfig.json index 31fda871..fc21f175 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,9 @@ "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitOverride": true } } diff --git a/vite.config.ts b/vite.config.ts index cd4e3f88..964d849c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,12 @@ import { existsSync, readdirSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; import { transform } from "esbuild"; import { defineConfig } from "vite-plus"; import type { PackUserConfig } from "vite-plus/pack"; import { webdriverio } from "vite-plus/test/browser-webdriverio"; -const rootDir = dirname(fileURLToPath(import.meta.url)); +const rootDir = import.meta.dirname; const srcDir = resolve(rootDir, "src"); type PackPlugin = Extract, unknown[]>[number]; @@ -17,12 +16,13 @@ type PackPlugin = Extract, unknown[]>[num * twin (that only happens for the `cjs` format). The bundled declaration has no * format-specific syntax, so a same-content copy keeps CommonJS consumers off TypeScript's * "masquerading as ESM" error under `moduleResolution: node16`/`nodenext`. + * @returns {PackPlugin} The pack plugin that emits the `.d.cts` twin. */ const emitCjsDtsTwin = (): PackPlugin => ({ name: "brazilian-utils:emit-cjs-dts-twin", generateBundle(_options, bundle) { - const dts = bundle["brazilian-utils.d.ts"]; - if (!dts) return; + const dts: (typeof bundle)[string] | undefined = bundle["brazilian-utils.d.ts"]; + if (dts === undefined) return; this.emitFile({ type: "asset", fileName: "brazilian-utils.d.cts", @@ -35,6 +35,7 @@ const emitCjsDtsTwin = (): PackPlugin => ({ * The UMD bundle is consumed as-is from a CDN `