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/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(""); }); }); 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 `