Description
isValidCnpj(cnpj, { version: 2 }) rejects lowercase alphanumeric CNPJs, even though cleanCnpj contains explicit code to uppercase them.
Reproduction
import { isValidCnpj } from "@brazilian-utils/brazilian-utils";
isValidCnpj("12ABC34501DE35", { version: 2 }); // true -> ok
isValidCnpj("12.ABC.345/01DE-35", { version: 2 }); // true -> ok
isValidCnpj("12abc34501de35", { version: 2 }); // false -> expected true
isValidCnpj("12.abc.345/01de-35", { version: 2 }); // false -> expected true
12.ABC.345/01DE-35 is the example published by Receita Federal in IN RFB 2.229/2024.
Root cause
In src/is-valid-cnpj/is-valid-cnpj.ts, cleanCnpj goes out of its way to normalise lowercase letters to uppercase:
result += char >= "a" && char <= "z" ? String.fromCharCode(char.charCodeAt(0) - 32) : char;
and the checksum computed from that cleaned value is correct:
cleanCnpj("12abc34501de35") === "12ABC34501DE35" // ok
isValidChecksum("12ABC34501DE35") === true // ok
But the final guard runs the format regex against the original input rather than the cleaned one:
return hasLetter && FORMAT_REGEX.test(cnpj) && isValidChecksum(cleaned);
// ^^^^^^^^^^^^^^^^^^^^^^ original, not `cleaned`
and FORMAT_REGEX has neither the i flag nor a lowercase range:
const FORMAT_REGEX = /^[0-9A-Z]{2}\.?[0-9A-Z]{3}\.?[0-9A-Z]{3}\/?[0-9A-Z]{4}-?[0-9]{2}$/;
So the lowercase branch inside cleanCnpj is effectively dead code on the alphanumeric path: any input that needs it is rejected downstream, before the cleaned value is used for anything other than the checksum.
The numeric path is unaffected -- NUMERIC_FORMAT_REGEX is digits-only, so case never comes up there.
Why tests don't catch it
The round-trip test drives the validator with generateCnpj(version), which only emits uppercase, so lowercase input is never exercised.
Which behaviour is intended?
The two pieces of code disagree, so I'd rather ask before sending a PR:
- Accept lowercase (what
cleanCnpj implies) -- add the i flag to FORMAT_REGEX.
- Reject lowercase (RFB writes the alphanumeric CNPJ in uppercase) -- then the
a-z handling in cleanCnpj is dead and should be removed, so the intent is explicit.
Happy to open a PR with a regression test for whichever you prefer.
Unrelated to #498, which touches generate-cnpj only.
Description
isValidCnpj(cnpj, { version: 2 })rejects lowercase alphanumeric CNPJs, even thoughcleanCnpjcontains explicit code to uppercase them.Reproduction
12.ABC.345/01DE-35is the example published by Receita Federal in IN RFB 2.229/2024.Root cause
In
src/is-valid-cnpj/is-valid-cnpj.ts,cleanCnpjgoes out of its way to normalise lowercase letters to uppercase:and the checksum computed from that cleaned value is correct:
But the final guard runs the format regex against the original input rather than the cleaned one:
and
FORMAT_REGEXhas neither theiflag nor a lowercase range:So the lowercase branch inside
cleanCnpjis effectively dead code on the alphanumeric path: any input that needs it is rejected downstream, before the cleaned value is used for anything other than the checksum.The numeric path is unaffected --
NUMERIC_FORMAT_REGEXis digits-only, so case never comes up there.Why tests don't catch it
The round-trip test drives the validator with
generateCnpj(version), which only emits uppercase, so lowercase input is never exercised.Which behaviour is intended?
The two pieces of code disagree, so I'd rather ask before sending a PR:
cleanCnpjimplies) -- add theiflag toFORMAT_REGEX.a-zhandling incleanCnpjis dead and should be removed, so the intent is explicit.Happy to open a PR with a regression test for whichever you prefer.
Unrelated to #498, which touches
generate-cnpjonly.