Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Catch more unsafe numbers #71

Merged
merged 10 commits into from
Dec 7, 2024
Merged

feat: Catch more unsafe numbers #71

merged 10 commits into from
Dec 7, 2024

Conversation

hildjj
Copy link
Contributor

@hildjj hildjj commented Dec 3, 2024

Prerequisites checklist

What is the purpose of this pull request?

Catch subnormals, numbers that are flushed to zero,
and overly-large integers in no-unsafe-values.

What changes did you make? (Give an overview)

Added three numeric cases to no-unsafe-values.js:

  • Flushed to zero: the parsed value is zero, but the original text implies that zero was not intended. Example: 1e-400
  • Overly-large integers: the original text looks like an integer (no decimal point or exponent), but the value is outside the range [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]. Example: 9007199254740992
  • Subnormals: these are numbers that have reduced precision in an IEEE-754 f64. Example: 2.2250738585072009e-308

Related Issues

Refs #68

Is there anything you'd like reviewers to focus on?

Please double-check my error messages for your preferences.

Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job on this. I just left a few notes mostly for cleanup.

@@ -3,6 +3,10 @@
* @author Bradley Meck Farias
*/

// These numbers evaluated to zero unexpectedly. If the group's digits
// are all zero, that's fine.
const SMALL_NUMBERS = [/\.(\d+)/u, /((?:\d+\.)?\d+)e/iu];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to match the entire number sequence? If not, can you add some comments explaining what patterns these regexes are meant to match?

Suggested change
const SMALL_NUMBERS = [/\.(\d+)/u, /((?:\d+\.)?\d+)e/iu];
const SMALL_NUMBERS = [/^\.(\d+)$/u, /^((?:\d+\.)?\d+)e$/iu];

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are intentionally not anchored. I'll add some comments and examples inline there to make it more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just rewritten this whole section to make it more clear and less error-prone, I hope. I just translated the ABNF for number into an anchored regex.

@@ -13,6 +17,8 @@ export default {

messages: {
unsafeNumber: "Number outside safe range found.",
unsafeInteger: "Integer outside safe range found.",
subnormal: "Subnormal numbers are outside the safe range.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to give folks a bit more here because I'm guessing a lot of people don't know what "subnormal" means.

Suggested change
subnormal: "Subnormal numbers are outside the safe range.",
subnormal: "Unexpected subnormal number '{{value}}` found.",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, almost nobody deals with these enough, which is why I think this particular lint is so valuable. Perhaps a link to https://en.wikipedia.org/wiki/Subnormal_number as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally don't put links in the error messages, but I think it would be good to add into the rule description. (At some point we'll create a page for each rule, and that would be a good place to go into more detail.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, I didn't see this until I just sent the update. Willfix.

Comment on lines 40 to 43
for (const r of SMALL_NUMBERS) {
const m = txt.match(r);
// If the digits are all 0, it's ok.
if (m?.[1]?.match(/[1-9]/u)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just eliminating one-character variables.

Suggested change
for (const r of SMALL_NUMBERS) {
const m = txt.match(r);
// If the digits are all 0, it's ok.
if (m?.[1]?.match(/[1-9]/u)) {
for (const pattern of SMALL_NUMBERS) {
const match = txt.match(pattern);
// If the digits are all 0, it's ok.
if (match?.[1]?.match(/[1-9]/u)) {

Comment on lines 64 to 69
const ab = new ArrayBuffer(8);
const dv = new DataView(ab);
dv.setFloat64(0, node.value, false);
const bi = dv.getBigUint64(0, false);
// Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
if ((bi & 0x7ff0000000000000n) === 0n) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanding variable names.

Suggested change
const ab = new ArrayBuffer(8);
const dv = new DataView(ab);
dv.setFloat64(0, node.value, false);
const bi = dv.getBigUint64(0, false);
// Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
if ((bi & 0x7ff0000000000000n) === 0n) {
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, node.value, false);
const bigInt = view.getBigUint64(0, false);
// Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
if ((bigInt & 0x7ff0000000000000n) === 0n) {

@@ -13,6 +19,11 @@ export default {

messages: {
unsafeNumber: "Number outside safe range found.",
unsafeInteger: "Integer outside safe range found.",
unsafeZero:
"This number will evaluate to zero, which is unintended.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include the number to make this a bit clearer?

Suggested change
"This number will evaluate to zero, which is unintended.",
"The number '{{ value }}` will evaluate to zero.",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's nothing interesting in the number. It's either going to be exactly the same as the highlighted text (if we use txt) or 0 (if we use node.value)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true when people are using ESLint in an editor. People also use ESLint in their CI system, where it just outputs messages onto the terminal. In that case, having the value in the message is really helpful.

unsafeZero:
"This number will evaluate to zero, which is unintended.",
subnormal:
"Subnormal numbers (see https://en.wikipedia.org/wiki/Subnormal_number) are outside the safe range: '{{ value }}'.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't put URLs in messages as that makes the message more difficult to read.

Suggested change
"Subnormal numbers (see https://en.wikipedia.org/wiki/Subnormal_number) are outside the safe range: '{{ value }}'.",
"Unexpected subnormal number '{{ value }}' found. Subnormal numbers are outside the safe range.",

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some tests for unsafeZero?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I had done that, but didn't get the file in the commit. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I do that all the time!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I do that all the time!

@@ -13,6 +19,11 @@ export default {

messages: {
unsafeNumber: "Number outside safe range found.",
unsafeInteger: "Integer outside safe range found.",
unsafeZero:
"This number will evaluate to zero, which is unintended.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true when people are using ESLint in an editor. People also use ESLint in their CI system, where it just outputs messages onto the terminal. In that case, having the value in the message is really helpful.

@@ -14,7 +14,8 @@ export default {
type: /** @type {const} */ ("problem"),

docs: {
description: "Disallow JSON values that are unsafe for interchange",
description:
"Disallow JSON values that are unsafe for interchange. This includes strings with unmatched surrogates, numbers that evaluate to Infinity, numbers that evaluate to zero unintentionally, numbers that look like integers but they are too large, and subnormal numbers (see: https://en.wikipedia.org/wiki/Subnormal_number)",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Descriptions are meant to be one sentence. I think the original description was fine. The rest of these details should go in the README.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is going to modify the readme, I'm going to rebase on top of main. Hopefully the patch isn't large enough that creates a problem.

When the test was copied from a previous example,
the name in the comments wasn't changed.
Flag subnormals, numbers that are flushed to zero,
and overly-large integers.

Refs eslint#68
@hildjj
Copy link
Contributor Author

hildjj commented Dec 4, 2024

Sorry forgot to fix the value issue also. On it.

@hildjj
Copy link
Contributor Author

hildjj commented Dec 4, 2024

I made the rest of the errors consistent at the same time. Tests modified.

nzakas
nzakas previously approved these changes Dec 4, 2024
Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Waiting for a second review before merging.

data: { value },
});
}
} else if (!value.match(/[.e]/iu)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (!value.match(/[.e]/iu)) {
} else if (!/[.e]/iu.test(value)) {

Since we're only checking, so we don't need the match.

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 has a non-zero digit
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some text is missing between "the" and "has"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"exponent". fixing.

Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving. Waiting for @mdjermanovic to verify his comments have been addressed.

Copy link
Member

@mdjermanovic mdjermanovic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@mdjermanovic mdjermanovic merged commit 5ffc7c0 into eslint:main Dec 7, 2024
16 checks passed
@github-actions github-actions bot mentioned this pull request Dec 4, 2024
@hildjj hildjj deleted the fix-68 branch December 7, 2024 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Complete
Development

Successfully merging this pull request may close these issues.

3 participants