README
eslintpluginbignumberrules
bignumber.js (or something similar!) instead of native JavaScript arithmetic and Math functions:
โ Enforce ๐ฐ financesafe ๐งท calculations using[ Customisable!  Noisy!  Financesafe? ]
# Installation:
$ pnpm i eslintpluginbignumberrules savedev
Configuration
After installation, make the plugin available to your eslint
:
// .eslintrc
{
"plugins": ["bignumberrules"]
}
Recommended rules will warn
about everything:
// .eslintrc
{
"plugins": ["bignumberrules"],
"extends": ["plugin:bignumberrules/recommended"]
}
"Everything" means this:
// .eslintrc
{
"plugins": ["bignumberrules"],
"rules": {
"bignumberrules/arithmetic": "warn",
"bignumberrules/assignment": "warn",
"bignumberrules/isNaN": "warn",
"bignumberrules/math": "warn",
"bignumberrules/number": "warn",
"bignumberrules/parseFloat": "warn",
"bignumberrules/rounding": "warn"
},
"settings": {
"bignumberrules": {
// Specify the following if you want rules to apply only
// to files with an `import 'bignumber.js'` declaration
"importDeclaration": "bignumber.js"
}
}
}
You can also customise the transformations.
Example transforms:
// Regular arithmetic operators:
//
0.1 + 0.2 > BigNumber.sum(0.1, 0.2)
19.99 * 0.1 > BigNumber(19.99).multipliedBy(0.1)
1 < 2 > BigNumber(1).isLessThan(2)
// Can keep a chain going...
//
BigNumber.sum(0.1, 0.2)  0.3
> BigNumber.sum(0.1, 0.2).minus(0.3)
3 ** BigNumber(1).plus(2)
> BigNumber(3).exponentiatedBy(BigNumber(1).plus(2))
// Bitshifting...
//
2 >>> 4 > BigNumber(2).shiftedBy(4)
4 << 2 > BigNumber(4).shiftedBy(2)
// Math methods...
//
Math.min(1, 2) > BigNumber.minimum(1, 2)
Math.sign(6) > BigNumber(6).comparedTo(0)
// toFixed + parseFloat...
//
;(1).toFixed(2) > BigNumber(1).decimalPlaces(2)
parseFloat('1.2') > BigNumber('1.2')
Number.parseFloat('2.1') > BigNumber('2.1')
What about Math.round(), ceil, floor?
That works, just not for bignumber.js
.
The big.js config supports transformations:
// Math.round(1.5)
// Math.ceil(1.5)
// Math.floor(1.5)
//
// ...becomes:
Big.round(1.5, 1) // 1 = half_up (round)
Big.round(1.5, 3) // 3 = up (ceil)
Big.round(1.5, 0) // 0 = down (floor)
However, bignumber.js
configures its roundingmode by setting an option in its constructor. The plugin can't perform a replacement in this case, so it warns you instead:
bignumberrules/rounding
46:1 warning is 'Math.round(10)' a financial calculation?
If so, use the global constructor setting:
BigNumber.set({
ROUNDING_MODE: BigNumber.ROUND_HALF_UP
})
Look for the supportsRound
setting in the example configs.
Limiting the number of warnings
Since 1.6.0 the plugin supports an importDeclaration
option. If specified, rules will only apply to files that include an import statement that matches it:
For example:
// .eslintrc
{
"plugins": ["bignumberrules"],
"settings": {
"bignumberrules": {
"importDeclaration": "bignumber.js"
}
}
}
Now, rules will only be applied to files that have the following import:
import BigNumber from 'bignumber.js'
For now this is ESM only, so it won't work with require()
I'm afraid.
By default, importDeclaration
is set to "__IGNORE__"
, meaning all files that eslint is interested in will be processed.
Leaving this default in place on a large project will likely result in looooads of warnings. It's not like we use ===
just for arithmetic, right? :)
There are a few strategies we can employ to keep the number of warnings down to something useful:
Read all of the warnings and address the ones that need addressing
Add linebyline and filebyfile ignore comments
Centralise your calculations into a few files only
Taken inorder I think these constitute a good approach to ending up with a financesafe codebase: Identify what needs fixing, fix them, and refactor the calculations as you go into more centralised places.
You can then use a combination of importDeclaration
and eslint's ruleenabling comment syntax to do things filebyfile, for example:
// sum.js
import BigNumber from 'bignumber.js'
const sum = 1 + 2
// ^^^^^  Is this a financial calculation?
// (bignumberrules/arithmetic)
...
// .eslintrc
{
"plugins": ["bignumberrules"],
"settings": {
"bignumberrules": {
"importDeclaration": "bignumber.js"
}
}
}
Any other caveats?
You may need to tweak some of the generated output.
For example, while developing the plugin I got this:
1 + 2 + 3  4
// autofixes to:
BigNumber(BigNumber.sum(1, 2, 3)).minus(4)
This is valid, but the parser now produces the more efficient:
BigNumber.sum(1, 2, 3).minus(4)
I'm not much of a hotshot with AST parsing, so you may encounter more weirdness like this. Contributions welcome. :)
Customisation
Want to use something other than bignumber.js
? Or use its shorter methodnames such as pow
and div
instead of exponentiatedBy
and dividedBy
?
Here's a config that works with big.js:
// .eslintrc
{
"plugins": ["bignumberrules"],
"settings": {
"bignumberrules": {
"construct": "Big",
"importDeclaration": "__IGNORE__",
"supportsSum": false,
"supportsBitwise": false,
"supportsRound": true,
"arithmetic": {
"+": "plus",
"": "minus",
"/": "div",
"*": "times",
"**": "pow",
"%": "mod"
},
"assignment": {
"+=": "plus",
"=": "minus",
"/=": "div",
"*=": "times",
"**=": "pow",
"%=": "mod"
},
"comparison": {
"<": "lt",
"<=": "lte",
"===": "eq",
"==": "eq",
"!==": ["__NEGATION__", "${L}", "eq", "${R}"],
"!=": ["__NEGATION__", "${L}", "eq", "${R}"],
">=": "gte",
">": "gt"
},
"math": {
"min": "min",
"max": "max",
"random": "NOT_SUPPORTED",
"abs": "abs",
"sign": ["__CONSTRUCT__(${A}).cmp(0)"],
"sqrt": "sqrt"
},
"rounding": {
"round": ["round", "${A}, 1"],
"ceil": ["round", "${A}, 3"],
"floor": ["round", "${A}, 0"]
},
"number": {
"parseFloat": ["__CONSTRUCT__(${A})"],
"toExponential": "toExponential",
"toFixed": "dp",
"toPrecision": "toPrecision",
"toString": "toString"
}
}
}
}
Find more examples in the /eslintrcforotherlibs folder.
Please report an issue if your particular lib does something differently!
There can't be that many edgecases, right? ;)
But why?
If you use floatingpoints for currency (instead of wholenumbers like you probably should) libraries like bignumber.js help keep your code away from the binary floatingpoint pitfalls of IEEE754:
const sum = 0.1 + 0.2
sum === 0.3
// false
sum
// 0.30000000000000004
This is the classic example and is often cited, but there are other rare cornercases that will eventually be caught some time after committing to a currencyunsafe solution.
eslintpluginbignumberrules
will translate the example above to:
const sum = BigNumber.sum(0.1, 0.2)
BigNumber(sum).isEqualTo(0.3)
// true
The problem manifests in the first place because in the floatingpoint numbertype of most languages (not just JavaScript!) the mantissa/significand is represented as a poweroftwo fraction rather than a powerof10 decimal:
_ _._____._____._____._____._____._____._____.______.______.__ _ _
_ _ 8  4  2  1  1/2  1/4  1/8  1/16  1/32  ... etc
\__________.___________/ \______________________________ _ _ _
Exponent ^ 

Significand >>^
IEEE754 defines various rules for marshalling these fractions into a decimal, but as you can probably imagine it's not always exact.
Libraries like bignumber.js
helps us work around this. Using them isn't complicated, but it does require a little discipline and vigilance to keep on top of, so an eslint plugin to warnabout the use of JavaScript's nativemath methods seemed like a good way to do that.
Credits
eslintpluginbignumberrules
was written by Conan Theobald.
He was inspired by the work of these fine Internet folk:
๐
Contributing
To support my efforts with this project, consider checking out the accountancy company I work for: Crunch.
License
MIT licensed: See LICENSE