Skip to content

Commit

Permalink
feat: Catch more unsafe numbers (#71)
Browse files Browse the repository at this point in the history
* test: Drive-by nit to fix test name

When the test was copied from a previous example,
the name in the comments wasn't changed.

* feat: Catch more unsafe numbers

Flag subnormals, numbers that are flushed to zero,
and overly-large integers.

Refs #68

* Rework unsafe zero.  Fix code review issues.

* Add a few more tests

* Rework error text

* Move detailed description of no-unsafe-values to readme.

* fmt

* Add values to errors

* Make links in readme

* code review issues
  • Loading branch information
hildjj authored Dec 7, 2024
1 parent af56d6c commit 5ffc7c0
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 3 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@ export default [

- `no-duplicate-keys` - warns when there are two keys in an object with the same text.
- `no-empty-keys` - warns when there is a key in an object that is an empty string or contains only whitespace (note: `package-lock.json` uses empty keys intentionally)
- `no-unsafe-values` - warns on values that are unsafe for interchange, such as numbers outside safe range or lone surrogates.
- `no-unsafe-values` - warns on values that are unsafe for interchange, such
as strings with unmatched
[surrogates](https://en.wikipedia.org/wiki/UTF-16), numbers that evaluate to
Infinity, numbers that evaluate to zero unintentionally, numbers that look
like integers but are too large, and
[subnormal numbers](https://en.wikipedia.org/wiki/Subnormal_number).
- `no-unnormalized-keys` - warns on keys containing [unnormalized characters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize#description). You can optionally specify the normalization form via `{ form: "form_name" }`, where `form_name` can be any of `"NFC"`, `"NFD"`, `"NFKC"`, or `"NFKD"`.
- `top-level-interop` - warns when the top-level item in the document is neither an array nor an object. This can be enabled to ensure maximal interoperability with the oldest JSON parsers.

Expand Down
66 changes: 65 additions & 1 deletion src/rules/no-unsafe-values.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
* @author Bradley Meck Farias
*/

// RFC 8259's `number` production, as a regex. Capture the integer part
// and the fractional part.
const NUMBER =
/^-?(?<int>0|([1-9][0-9]*))(?:\.(?<frac>[0-9]+))?(?:[eE][+-]?[0-9]+)?$/u;
const NON_ZERO = /[1-9]/u;

export default {
meta: {
type: /** @type {const} */ ("problem"),
Expand All @@ -12,19 +18,77 @@ export default {
},

messages: {
unsafeNumber: "Number outside safe range found.",
unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.",
unsafeInteger:
"The integer '{{ value }}' is outside the safe integer range.",
unsafeZero: "The number '{{ value }}' will evaluate to zero.",
subnormal:
"Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.",
loneSurrogate: "Lone surrogate '{{ surrogate }}' found.",
},
},

create(context) {
return {
Number(node) {
const value = context.sourceCode.getText(node);

if (Number.isFinite(node.value) !== true) {
context.report({
loc: node.loc,
messageId: "unsafeNumber",
data: { value },
});
} else {
// Also matches -0, intentionally
if (node.value === 0) {
// If the value has been rounded down to 0, but there was some
// fraction or non-zero part before the e-, this is a very small
// number that doesn't fit inside an f64.
const match = value.match(NUMBER);
// assert(match, "If the regex is right, match is always truthy")

// If any part of the number other than the exponent has a
// non-zero digit in it, this number was not intended to be
// evaluated down to a zero.
if (
NON_ZERO.test(match.groups.int) ||
NON_ZERO.test(match.groups.frac)
) {
context.report({
loc: node.loc,
messageId: "unsafeZero",
data: { value },
});
}
} else if (!/[.e]/iu.test(value)) {
// Intended to be an integer
if (
node.value > Number.MAX_SAFE_INTEGER ||
node.value < Number.MIN_SAFE_INTEGER
) {
context.report({
loc: node.loc,
messageId: "unsafeInteger",
data: { value },
});
}
} else {
// Floating point. Check for subnormal.
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, node.value, false);
const asBigInt = view.getBigUint64(0, false);
// Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
if ((asBigInt & 0x7ff0000000000000n) === 0n) {
context.report({
loc: node.loc,
messageId: "subnormal",
// Value included so that it's seen in scientific notation
data: node,
});
}
}
}
},
String(node) {
Expand Down
146 changes: 145 additions & 1 deletion tests/rules/no-unsafe-values.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @fileoverview Tests for no-empty-keys rule.
* @fileoverview Tests for no-unsafe-values rule.
* @author Bradley Meck Farias
*/

Expand Down Expand Up @@ -35,13 +35,19 @@ ruleTester.run("no-unsafe-values", rule, {
},
'"🔥"',
'"\\ud83d\\udd25"',
"0.00000",
"0e0000000",
"0.00000e0000",
],
invalid: [
{
code: "2e308",
errors: [
{
messageId: "unsafeNumber",
data: {
value: "2e308",
},
line: 1,
column: 1,
endLine: 1,
Expand All @@ -54,6 +60,9 @@ ruleTester.run("no-unsafe-values", rule, {
errors: [
{
messageId: "unsafeNumber",
data: {
value: "-2e308",
},
line: 1,
column: 1,
endLine: 1,
Expand Down Expand Up @@ -131,5 +140,140 @@ ruleTester.run("no-unsafe-values", rule, {
},
],
},
{
code: "1e-400",
errors: [
{
messageId: "unsafeZero",
data: {
value: "1e-400",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 7,
},
],
},
{
code: "-1e-400",
errors: [
{
messageId: "unsafeZero",
data: {
value: "-1e-400",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 8,
},
],
},
{
code: "0.01e-400",
errors: [
{
messageId: "unsafeZero",
data: {
value: "0.01e-400",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 10,
},
],
},
{
code: "-10.2e-402",
errors: [
{
messageId: "unsafeZero",
data: {
value: "-10.2e-402",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 11,
},
],
},
{
code: `0.${"0".repeat(400)}1`,
errors: [
{
messageId: "unsafeZero",
data: {
value: `0.${"0".repeat(400)}1`,
},
line: 1,
column: 1,
endLine: 1,
endColumn: 404,
},
],
},
{
code: "9007199254740992",
errors: [
{
messageId: "unsafeInteger",
data: {
value: "9007199254740992",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 17,
},
],
},
{
code: "-9007199254740992",
errors: [
{
messageId: "unsafeInteger",
data: {
value: "-9007199254740992",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 18,
},
],
},
{
code: "2.2250738585072009e-308",
errors: [
{
messageId: "subnormal",
data: {
value: "2.225073858507201e-308",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 24,
},
],
},
{
code: "-2.2250738585072009e-308",
errors: [
{
messageId: "subnormal",
data: {
value: "-2.225073858507201e-308",
},
line: 1,
column: 1,
endLine: 1,
endColumn: 25,
},
],
},
],
});

0 comments on commit 5ffc7c0

Please sign in to comment.