@pipeworx/mcp-edge-case-museum 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/bin/cli.js +55 -11
- package/lib/index.js +333 -0
- package/package.json +17 -16
- package/server.json +1 -1
- package/tools.json +98 -0
- package/src/bake.mjs +0 -112
- package/src/ecm-index-data.ts +0 -15
- package/src/exhibits/datetime/add-24-hours-vs-add-1-day-across-dst.json +0 -51
- package/src/exhibits/datetime/add-365-days-to-feb-29-not-next-feb-29.json +0 -49
- package/src/exhibits/datetime/age-calculator-feb29-birthday-crash-vs-silent.json +0 -52
- package/src/exhibits/datetime/century-leap-year-1900-not-leap.json +0 -68
- package/src/exhibits/datetime/datetime-utcnow-naive-deprecated.json +0 -39
- package/src/exhibits/datetime/dst-fall-back-ambiguous-fold.json +0 -51
- package/src/exhibits/datetime/dst-spring-forward-nonexistent-time.json +0 -57
- package/src/exhibits/datetime/fractional-utc-offsets-kathmandu-kolkata.json +0 -50
- package/src/exhibits/datetime/iso-week-year-vs-calendar-year.json +0 -48
- package/src/exhibits/datetime/js-date-field-overflow-rollover.json +0 -49
- package/src/exhibits/datetime/leap-second-rejected-vs-silently-invalid.json +0 -57
- package/src/exhibits/datetime/lord-howe-30-minute-dst.json +0 -47
- package/src/exhibits/datetime/midnight-that-doesnt-exist-sao-paulo.json +0 -53
- package/src/exhibits/datetime/python-naive-aware-comparison-typeerror.json +0 -46
- package/src/exhibits/datetime/samoa-skipped-day-2011.json +0 -50
- package/src/exhibits/datetime/southern-hemisphere-dst-reversed.json +0 -50
- package/src/exhibits/datetime/two-digit-year-century-pivot-mismatch.json +0 -49
- package/src/exhibits/datetime/year-2038-int32-overflow.json +0 -52
- package/src/exhibits/filenames/absolute-second-argument-resets-join.json +0 -62
- package/src/exhibits/filenames/bidi-override-extension-spoof.json +0 -58
- package/src/exhibits/filenames/case-insensitive-filesystem-pure-path-mismatch.json +0 -74
- package/src/exhibits/filenames/dotfile-has-no-extension.json +0 -61
- package/src/exhibits/filenames/extension-trap-double-extension.json +0 -62
- package/src/exhibits/filenames/filename-length-limits-bytes-not-chars.json +0 -66
- package/src/exhibits/filenames/leading-dash-filename-parsed-as-flag.json +0 -67
- package/src/exhibits/filenames/nfc-nfd-filename-lookup-mismatch.json +0 -68
- package/src/exhibits/filenames/ntfs-alternate-data-stream-colon.json +0 -59
- package/src/exhibits/filenames/ntpath-splitdrive-unc-paths.json +0 -58
- package/src/exhibits/filenames/nul-byte-in-filename-raises-at-open.json +0 -60
- package/src/exhibits/filenames/path-join-escapes-the-base-directory.json +0 -63
- package/src/exhibits/filenames/windows-backslash-traversal-ntpath.json +0 -62
- package/src/exhibits/filenames/windows-reserved-device-names.json +0 -73
- package/src/exhibits/filenames/windows-trailing-dot-space-stripped.json +0 -77
- package/src/exhibits/float-money/decimal-from-float-vs-string.json +0 -66
- package/src/exhibits/float-money/float-anchor-0-1-plus-0-2.json +0 -67
- package/src/exhibits/float-money/intl-number-format-de-de-roundtrip-misparsed.json +0 -62
- package/src/exhibits/float-money/iso4217-minor-units-not-always-two.json +0 -78
- package/src/exhibits/float-money/math-abs-negative-zero.json +0 -51
- package/src/exhibits/float-money/max-safe-integer-plus-two.json +0 -62
- package/src/exhibits/float-money/naive-loop-sum-order-dependence.json +0 -71
- package/src/exhibits/float-money/nan-breaks-equality-and-max-min.json +0 -70
- package/src/exhibits/float-money/negative-zero-balance-formatting.json +0 -74
- package/src/exhibits/float-money/number-epsilon-false-positive-small-magnitude.json +0 -52
- package/src/exhibits/float-money/parsefloat-comma-thousands-separator.json +0 -66
- package/src/exhibits/float-money/python-banker-rounding-vs-js-round-half-up.json +0 -65
- package/src/exhibits/float-money/python-round-2-675-float-imprecision.json +0 -64
- package/src/exhibits/float-money/splitting-100-dollars-three-ways-leaves-a-cent.json +0 -63
- package/src/exhibits/float-money/sum-ten-dimes-not-a-dollar.json +0 -77
- package/src/exhibits/float-money/tofixed-1-005-rounds-down-not-up.json +0 -63
- package/src/exhibits/float-money/tofixed-2-345-inconsistent-rounding.json +0 -64
- package/src/exhibits/integers/array-index-above-2-32-2-not-an-index.json +0 -42
- package/src/exhibits/integers/bigint-number-mixing-typeerror.json +0 -49
- package/src/exhibits/integers/bitwise-32-bit-truncation.json +0 -56
- package/src/exhibits/integers/bool-is-int-subclass-dict-key-collision.json +0 -58
- package/src/exhibits/integers/floor-div-mod-sign-python-vs-js.json +0 -48
- package/src/exhibits/integers/json-parse-loses-id-precision.json +0 -58
- package/src/exhibits/integers/max-safe-integer-plus-one-equals-plus-two.json +0 -58
- package/src/exhibits/integers/number-empty-and-whitespace-string-equals-zero.json +0 -55
- package/src/exhibits/integers/parseint-null-radix36-digit-soup.json +0 -57
- package/src/exhibits/integers/parseint-number-argument-scientific-notation.json +0 -48
- package/src/exhibits/integers/parseint-stops-at-first-invalid-char-vs-number.json +0 -58
- package/src/exhibits/integers/python-int-accepts-fullwidth-unicode-digits.json +0 -56
- package/src/exhibits/integers/python-int-accepts-underscores-and-whitespace.json +0 -48
- package/src/exhibits/integers/python-unbounded-int-vs-fixed-width-wrap.json +0 -49
- package/src/exhibits/names-addresses/address-with-no-street-number.json +0 -54
- package/src/exhibits/names-addresses/ascii-regex-rejects-accented-and-hyphenated-names.json +0 -55
- package/src/exhibits/names-addresses/house-number-suffix-collapsed-by-digit-extraction.json +0 -55
- package/src/exhibits/names-addresses/mononym-fails-first-last-required.json +0 -56
- package/src/exhibits/names-addresses/naive-lowercase-breaks-greek-final-sigma.json +0 -64
- package/src/exhibits/names-addresses/null-surname-coerced-to-null-value.json +0 -58
- package/src/exhibits/names-addresses/single-letter-surname-fails-min-length.json +0 -55
- package/src/exhibits/names-addresses/sort-by-surname-particle-convention-mismatch.json +0 -57
- package/src/exhibits/names-addresses/spanish-double-surname-treated-as-single.json +0 -55
- package/src/exhibits/names-addresses/split-surname-on-last-space-loses-particle.json +0 -59
- package/src/exhibits/names-addresses/title-case-mangles-mixed-case-and-apostrophes.json +0 -60
- package/src/exhibits/names-addresses/uk-canada-postal-code-assumed-numeric.json +0 -55
- package/src/exhibits/names-addresses/us-zip-code-leading-zero-lost-as-integer.json +0 -58
- package/src/exhibits/names-addresses/varchar-truncation-splits-name-mid-grapheme.json +0 -63
- package/src/exhibits/parsing/csv-crlf-trailing-blank-line.json +0 -87
- package/src/exhibits/parsing/csv-doubled-quote-escaping.json +0 -70
- package/src/exhibits/parsing/csv-formula-injection-excel.json +0 -71
- package/src/exhibits/parsing/csv-quoted-field-with-comma.json +0 -70
- package/src/exhibits/parsing/csv-quoted-field-with-newline.json +0 -70
- package/src/exhibits/parsing/csv-semicolon-delimited-european-excel.json +0 -70
- package/src/exhibits/parsing/csv-utf8-bom-first-header.json +0 -74
- package/src/exhibits/parsing/email-regex-rejects-valid-apostrophe-address.json +0 -68
- package/src/exhibits/parsing/json-big-integer-precision-loss.json +0 -65
- package/src/exhibits/parsing/json-duplicate-keys-last-wins.json +0 -64
- package/src/exhibits/parsing/json-lone-surrogate-roundtrip.json +0 -58
- package/src/exhibits/parsing/json-nan-infinity-not-valid-json.json +0 -54
- package/src/exhibits/parsing/url-backslash-treated-as-slash-whatwg.json +0 -60
- package/src/exhibits/parsing/url-urlparse-never-raises-on-garbage.json +0 -60
- package/src/exhibits/parsing/url-userinfo-trusted-looking-host.json +0 -60
- package/src/exhibits/sorting/accented-letters-sort-after-z.json +0 -63
- package/src/exhibits/sorting/byte-order-caps-before-lowercase.json +0 -66
- package/src/exhibits/sorting/case-insensitive-sort-lower-vs-casefold.json +0 -56
- package/src/exhibits/sorting/comparator-returns-boolean-js.json +0 -49
- package/src/exhibits/sorting/js-array-sort-stringifies-numbers.json +0 -68
- package/src/exhibits/sorting/locale-collation-de-vs-sv.json +0 -54
- package/src/exhibits/sorting/localecompare-numeric-option.json +0 -50
- package/src/exhibits/sorting/nan-in-sort.json +0 -59
- package/src/exhibits/sorting/natural-sort-breaks-lexicographic.json +0 -63
- package/src/exhibits/sorting/postgres-collation-documented.json +0 -61
- package/src/exhibits/sorting/python-cannot-compare-str-int.json +0 -70
- package/src/exhibits/sorting/reverse-sort-vs-reverse-after-sort-ties.json +0 -48
- package/src/exhibits/sorting/sorting-mixed-none-null.json +0 -56
- package/src/exhibits/sorting/typed-array-sort-is-numeric-by-default.json +0 -48
- package/src/exhibits/unicode/arabic-indic-digit-parses.json +0 -73
- package/src/exhibits/unicode/bidi-override-filename-spoof.json +0 -62
- package/src/exhibits/unicode/cyrillic-homoglyph-paypal-spoof.json +0 -67
- package/src/exhibits/unicode/fullwidth-bypasses-keyword-filter.json +0 -61
- package/src/exhibits/unicode/german-eszett-case-fold.json +0 -72
- package/src/exhibits/unicode/grapheme-vs-codepoint-length.json +0 -57
- package/src/exhibits/unicode/greek-final-sigma-casefold.json +0 -54
- package/src/exhibits/unicode/js-word-regex-ascii-only.json +0 -64
- package/src/exhibits/unicode/nfc-nfd-same-name-two-accounts.json +0 -70
- package/src/exhibits/unicode/nfkc-compatibility-folding.json +0 -63
- package/src/exhibits/unicode/truncation-orphans-combining-mark.json +0 -62
- package/src/exhibits/unicode/truncation-splits-surrogate-pair.json +0 -65
- package/src/exhibits/unicode/turkish-dotless-i-case-mapping.json +0 -72
- package/src/exhibits/unicode/zalgo-combining-mark-stacking.json +0 -57
- package/src/exhibits/unicode/zero-width-space-username.json +0 -62
- package/src/harness.mjs +0 -331
- package/src/index.ts +0 -686
- package/src/server.ts +0 -49
- package/src/verified.json +0 -977
- package/tsconfig.json +0 -18
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "tofixed-2-345-inconsistent-rounding",
|
|
3
|
-
"title": "2.345 rounds UP to 2.35 while 1.005 rounds DOWN to 1.00 - same \"x.xx5\" shape, opposite outcome",
|
|
4
|
-
"category": "float-money",
|
|
5
|
-
"assumption": "Any price ending in exactly half a cent (x.xx5) rounds the same way - either always up, or always down.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "(2.345).toFixed(2) vs (1.005).toFixed(2)",
|
|
8
|
-
"description": "Two different prices that both look like a clean half-cent boundary, rounded to cents for display."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "2.345 rounds to \"2.35\" (up) but 1.005 rounds to \"1.00\" (down), even though both are written with the same \"value.value5\" shape - there is no way to predict which direction a given half-cent literal will round without checking its actual binary value.",
|
|
11
|
-
"why": "Neither 2.345 nor 1.005 is exactly representable in IEEE-754 double. 2.345's nearest double happens to be a hair above 2.345 (2.34500000000000019540...), so it's not actually a tie and rounds up; 1.005's nearest double happens to be a hair below 1.005 (1.00499999999999989341...), so it rounds down. Both languages round the true stored binary value, not the decimal text you typed, and whether a given literal lands slightly above or below its decimal value is essentially unpredictable without inspecting the bits.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript",
|
|
14
|
-
"python",
|
|
15
|
-
"any IEEE-754 double formatted by rounding its true binary value"
|
|
16
|
-
],
|
|
17
|
-
"repros": [
|
|
18
|
-
{
|
|
19
|
-
"language": "javascript",
|
|
20
|
-
"code": "console.log((2.345).toFixed(2), (2.345).toFixed(20));\nconsole.log((1.005).toFixed(2), (1.005).toFixed(20));\n",
|
|
21
|
-
"expected_output": "2.35 2.34500000000000019540\n1.00 1.00499999999999989342",
|
|
22
|
-
"verified_with": "Node.js v22.22.2"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"language": "python",
|
|
26
|
-
"code": "for x in (2.345, 1.005):\n print(f\"{x:.2f}\", f\"{x:.20f}\")\n",
|
|
27
|
-
"expected_output": "2.35 2.34500000000000019540\n1.00 1.00499999999999989342",
|
|
28
|
-
"verified_with": "Python 3.14.2"
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"fix": "Never treat float literals as exact decimal values for money. Round and store amounts as integer minor units, or construct a Decimal from the original decimal string (not from a float), so rounding operates on the number you actually meant.",
|
|
32
|
-
"related": [
|
|
33
|
-
"tofixed-1-005-rounds-down-not-up",
|
|
34
|
-
"python-round-2-675-float-imprecision"
|
|
35
|
-
],
|
|
36
|
-
"sources": [
|
|
37
|
-
{
|
|
38
|
-
"title": "ECMA-262: Number.prototype.toFixed",
|
|
39
|
-
"url": "https://tc39.es/ecma262/#sec-number.prototype.tofixed"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"title": "Python docs: 15.1. Floating-Point Arithmetic: Issues and Limitations",
|
|
43
|
-
"url": "https://docs.python.org/3/tutorial/floatingpoint.html"
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"tags": [
|
|
47
|
-
"money",
|
|
48
|
-
"rounding",
|
|
49
|
-
"price",
|
|
50
|
-
"currency",
|
|
51
|
-
"invoice",
|
|
52
|
-
"display"
|
|
53
|
-
],
|
|
54
|
-
"test_cases": [
|
|
55
|
-
{
|
|
56
|
-
"inputs": [
|
|
57
|
-
"1.005"
|
|
58
|
-
],
|
|
59
|
-
"relation": "returns",
|
|
60
|
-
"expected": "1.01",
|
|
61
|
-
"note": "A money-rounding function that takes the exact decimal string \"1.005\" and rounds half-cents up should return \"1.01\" - if it instead parses to a float first and formats with toFixed, it will silently return \"1.00\"."
|
|
62
|
-
}
|
|
63
|
-
]
|
|
64
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "array-index-above-2-32-2-not-an-index",
|
|
3
|
-
"title": "arr[4294967295] = x does not grow a JavaScript array's length, but arr[4294967294] = x does",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "Any non-negative integer you assign as an array subscript is a real array index that updates .length.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "arr[2**32 - 2] = \"a\" updates .length to 2**32 - 1; arr[2**32 - 1] = \"b\" does NOT update .length at all",
|
|
8
|
-
"description": "Computing a very large array index programmatically (from a hash, an offset calculation, or a bug) and writing to it, expecting ordinary array growth semantics."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "Writing to what looks like 'one past the last valid huge index' silently creates an ordinary (non-index) property on the array object instead of extending the array, so `.length`, `for` loops bounded by `.length`, and array methods like `.forEach` never see that value at all.",
|
|
11
|
-
"why": "The ECMAScript spec defines an 'array index' as a string that is a canonical numeric representation of an integer in the range 0 to 2^32 - 2 inclusive -- 2^32 - 1 is deliberately excluded, because an array's `.length` is always an integer from 0 to 2^32 - 1 (setting it to 2^32 throws `RangeError`) and must be able to hold 'one past the largest index'. So the largest valid index is 2^32 - 2; assigning to 2^32 - 1 is legal (arrays allow arbitrary property keys) but does not count as an array index and therefore never triggers the `.length`-update behavior that real index assignment does.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "javascript",
|
|
18
|
-
"code": "const arr = [];\narr[4294967294] = \"a\"; // 2**32 - 2, the largest valid array index\nconsole.log(\"after valid index:\", arr.length);\narr[4294967295] = \"b\"; // 2**32 - 1, NOT a valid array index\nconsole.log(\"after 2**32 - 1:\", arr.length);\nconsole.log(\"both keys present:\", (4294967294 in arr), (4294967295 in arr));\nconsole.log(\"own enumerable keys:\", Object.keys(arr).length);\n",
|
|
19
|
-
"expected_output": "after valid index: 4294967295\nafter 2**32 - 1: 4294967295\nboth keys present: true true\nown enumerable keys: 2",
|
|
20
|
-
"verified_with": "Node.js v22.22.2"
|
|
21
|
-
}
|
|
22
|
-
],
|
|
23
|
-
"fix": "Never compute an array index without bounds-checking it against 2^32 - 2 first; if a value might legitimately be that large, use a Map or plain object keyed by string/number instead of relying on native array index semantics.",
|
|
24
|
-
"related": [
|
|
25
|
-
"max-safe-integer-plus-one-equals-plus-two"
|
|
26
|
-
],
|
|
27
|
-
"sources": [
|
|
28
|
-
{
|
|
29
|
-
"title": "ECMA-262: Array index definition",
|
|
30
|
-
"url": "https://tc39.es/ecma262/#array-index"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"title": "ECMA-262: Array Exotic Objects (length invariants)",
|
|
34
|
-
"url": "https://tc39.es/ecma262/#sec-array-exotic-objects"
|
|
35
|
-
}
|
|
36
|
-
],
|
|
37
|
-
"tags": [
|
|
38
|
-
"validation",
|
|
39
|
-
"database"
|
|
40
|
-
],
|
|
41
|
-
"test_cases": []
|
|
42
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "bigint-number-mixing-typeerror",
|
|
3
|
-
"title": "10n + 5 throws TypeError instead of just giving you 15",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "Any two things that look like numbers can be added together directly.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "10n + 5",
|
|
8
|
-
"description": "Mixing a value that came from a BigInt-returning source (e.g. a large parsed id, `Number.MAX_SAFE_INTEGER` workaround, or a library that returns BigInt) with an ordinary numeric literal elsewhere in arithmetic."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "Introducing a single `BigInt` value into a codebase that otherwise uses plain `number` breaks at the first arithmetic operation that mixes the two, with a runtime `TypeError` rather than a silent or compile-time-caught issue.",
|
|
11
|
-
"why": "`BigInt` and `Number` are deliberately kept as separate, non-interoperable types in JavaScript: implicitly coercing one to the other would either lose a BigInt's exactness (converting to Number) or silently change comparison/arithmetic semantics, so the spec requires an explicit conversion (`BigInt(5)` or `Number(10n)`) before the two can be combined with `+`, `-`, `*`, etc. (Comparison operators like `<` and `==` are a deliberate exception and do work across the two types.)",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "javascript",
|
|
18
|
-
"code": "try {\n console.log(10n + 5);\n} catch (e) {\n console.log(e.constructor.name + ':', e.message);\n}\nconsole.log(10n + BigInt(5));\nconsole.log(10n < 20); // comparison operators DO mix the types\n",
|
|
19
|
-
"expected_output": "TypeError: Cannot mix BigInt and other types, use explicit conversions\n15n\ntrue",
|
|
20
|
-
"verified_with": "Node.js v22.22.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "python",
|
|
24
|
-
"code": "# Python has only one built-in integer type, so there is nothing to mix:\n# int already has unlimited precision, no separate 'big integer' type exists.\nprint(10 + 5, type(10 + 5))\n",
|
|
25
|
-
"expected_output": "15 <class 'int'>",
|
|
26
|
-
"verified_with": "Python 3.14.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Convert explicitly at the boundary where a value becomes a BigInt (or stays a Number) and keep the two types from mixing in arithmetic; don't let a single BigInt-returning call silently propagate into number-typed code.",
|
|
30
|
-
"related": [
|
|
31
|
-
"max-safe-integer-plus-one-equals-plus-two"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "MDN: BigInt",
|
|
36
|
-
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"title": "ECMA-262: BigInt operators",
|
|
40
|
-
"url": "https://tc39.es/ecma262/#sec-ecmascript-language-expressions"
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"tags": [
|
|
44
|
-
"id",
|
|
45
|
-
"parsing",
|
|
46
|
-
"validation"
|
|
47
|
-
],
|
|
48
|
-
"test_cases": []
|
|
49
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "bitwise-32-bit-truncation",
|
|
3
|
-
"title": "~~ and |0 quietly turn a large number negative",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "~~x and x|0 are safe, fast shortcuts for 'truncate x to an integer' that work for any number.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "(2**31) | 0 === -2147483648; ~~(2**31) === -2147483648",
|
|
8
|
-
"description": "A common micro-optimization idiom (`~~x` or `x | 0` instead of `Math.trunc(x)` / `Math.floor(x)`) applied to a value that turns out to be at or above 2^31, such as a pixel coordinate, a large counter, or a computed array size."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "A value that should just be truncated toward zero (like `Math.trunc` would do) instead flips to a large negative number once it reaches 2^31, because the 'fast truncate' idiom is secretly doing 32-bit signed integer conversion, not generic truncation.",
|
|
11
|
-
"why": "The bitwise operators `~`, `|`, `&`, `^`, `<<` and `>>` first convert their (left) operands to a 32-bit signed integer (via ToInt32, which wraps modulo 2^32 and then reinterprets the top bit as the sign). `~~x` is just `~(~x)`, double bitwise-NOT, which forces this same 32-bit conversion twice and happens to cancel the bit-flip, leaving the ToInt32 result -- so both idioms silently wrap any value outside the 32-bit signed range [-2^31, 2^31 - 1] instead of just dropping its fractional part. The unsigned shift `>>>` uses ToUint32 instead, so `x >>> 0` wraps into [0, 2^32 - 1] rather than going negative (`-1 >>> 0` is 4294967295).",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "javascript",
|
|
18
|
-
"code": "console.log((2 ** 31) | 0);\nconsole.log(~~(2 ** 31));\nconsole.log(Math.trunc(2 ** 31)); // the actually-safe way to truncate\nconsole.log((-5.7) | 0, ~~(-5.7)); // for in-range values, both idioms do look like truncation\n",
|
|
19
|
-
"expected_output": "-2147483648\n-2147483648\n2147483648\n-5 -5",
|
|
20
|
-
"verified_with": "Node.js v22.22.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "python",
|
|
24
|
-
"code": "# Python has no implicit 32-bit integer conversion; ints are arbitrary precision\n# unless you explicitly wrap them (see the struct/ctypes exhibit).\nprint(2**31)\nprint(int(2**31)) # no truncation needed, no wraparound\n",
|
|
25
|
-
"expected_output": "2147483648\n2147483648",
|
|
26
|
-
"verified_with": "Python 3.14.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Use `Math.trunc()` (or `Math.floor()`/`Math.round()` as appropriate) to truncate a number, and reserve bitwise operators for values you know fit in 32 bits (e.g. actual bit flags, not general arithmetic results).",
|
|
30
|
-
"related": [
|
|
31
|
-
"python-unbounded-int-vs-fixed-width-wrap"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "ECMA-262: ToInt32",
|
|
36
|
-
"url": "https://tc39.es/ecma262/#sec-toint32"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"title": "MDN: Bitwise NOT (~)",
|
|
40
|
-
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_NOT"
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"tags": [
|
|
44
|
-
"validation",
|
|
45
|
-
"parsing"
|
|
46
|
-
],
|
|
47
|
-
"test_cases": [
|
|
48
|
-
{
|
|
49
|
-
"inputs": [
|
|
50
|
-
2147483648
|
|
51
|
-
],
|
|
52
|
-
"relation": "no_crash",
|
|
53
|
-
"note": "A 'truncate to integer' helper should not flip sign for inputs at or above 2^31; compare against Math.trunc."
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "bool-is-int-subclass-dict-key-collision",
|
|
3
|
-
"title": "{1: \"a\", True: \"b\"} has only one key in Python, because True equals 1",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "A boolean key and an integer key in a dictionary are always distinct entries, since booleans and integers are different types.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "{1: \"a\", True: \"b\"} -> {1: \"b\"}, a dict with exactly one entry",
|
|
8
|
-
"description": "Building a lookup table or counter dict keyed partly by integers and partly by booleans from mixed data (e.g. a CSV or JSON blob where some flag fields were coerced to 0/1 and others stayed as true/false)."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "Inserting entries keyed `1` and `True` (or `0` and `False`) into the same dict overwrites one with the other instead of creating two entries, silently losing data with no error.",
|
|
11
|
-
"why": "`bool` is a subclass of `int` in Python (`class bool(int)`), specifically so that `True` and `1` behave identically wherever an integer is expected. As a consequence, `True == 1` is `True`, and `hash(True) == hash(1)`, and Python dicts (and sets) de-duplicate keys by equality-and-hash together -- so `1` and `True` are, as far as a dict is concerned, the exact same key, and whichever value was assigned last wins.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"python"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "python",
|
|
18
|
-
"code": "print(isinstance(True, int), True == 1, hash(True) == hash(1))\nprint(True + True) # arithmetic coercion, unsurprising on its own\nd = {1: \"a\", True: \"b\"}\nprint(d, len(d))\n",
|
|
19
|
-
"expected_output": "True True True\n2\n{1: 'b'} 1",
|
|
20
|
-
"verified_with": "Python 3.14.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "javascript",
|
|
24
|
-
"code": "// JS booleans are not a number subtype: object keys coerce to strings, and\n// Map uses SameValueZero (no type coercion), so 1 and true never collide.\nconst obj = { 1: \"a\", true: \"b\" };\nconsole.log(obj, Object.keys(obj));\nconst m = new Map([[1, \"a\"], [true, \"b\"]]);\nconsole.log(m.size, [...m.keys()]);\nconsole.log(true + true); // arithmetic coercion still matches Python here\n",
|
|
25
|
-
"expected_output": "{ '1': 'a', true: 'b' } [ '1', 'true' ]\n2 [ 1, true ]\n2",
|
|
26
|
-
"verified_with": "Node.js v22.22.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Never key a dict/set by a mix of booleans and small integers if you need them distinct; normalize flag values to a single consistent type (e.g. always `int`, or always `bool`) before using them as keys.",
|
|
30
|
-
"related": [],
|
|
31
|
-
"sources": [
|
|
32
|
-
{
|
|
33
|
-
"title": "Python docs: bool -- Boolean Type",
|
|
34
|
-
"url": "https://docs.python.org/3/library/stdtypes.html#boolean-type-bool"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"title": "Python docs: Built-in Types -- Mapping Types (dict)",
|
|
38
|
-
"url": "https://docs.python.org/3/library/stdtypes.html#mapping-types-dict"
|
|
39
|
-
}
|
|
40
|
-
],
|
|
41
|
-
"tags": [
|
|
42
|
-
"database",
|
|
43
|
-
"validation",
|
|
44
|
-
"json"
|
|
45
|
-
],
|
|
46
|
-
"test_cases": [
|
|
47
|
-
{
|
|
48
|
-
"inputs": [
|
|
49
|
-
[
|
|
50
|
-
1,
|
|
51
|
-
true
|
|
52
|
-
]
|
|
53
|
-
],
|
|
54
|
-
"relation": "no_crash",
|
|
55
|
-
"note": "Any dict-building code keyed by mixed int/bool values should decide explicitly whether 1 and True are the same key."
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "floor-div-mod-sign-python-vs-js",
|
|
3
|
-
"title": "-7 // 2 is -4 and -7 % 2 is 1 in Python, but Math.trunc(-7 / 2) is -3 and -7 % 2 is -1 in JavaScript",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "Integer division of a negative number by a positive one rounds the same way in every language -- toward zero.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "Python: -7 // 2 == -4, -7 % 2 == 1. JavaScript: Math.trunc(-7 / 2) == -3, -7 % 2 == -1.",
|
|
8
|
-
"description": "Any pagination, bucketing, or hashing logic computed with integer division or modulo on a value that can be negative -- e.g. 'which page is item index i on' or 'which bucket does this signed offset fall into'."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "The same division expression applied to a negative dividend gives a different quotient AND a different-signed remainder depending on the language, so a bucketing or pagination formula ported between a Python backend and a JS frontend (or vice versa) silently puts negative-offset items in a different bucket on each side.",
|
|
11
|
-
"why": "Python's `//` is floor division: it rounds the mathematical quotient toward negative infinity, so -7 // 2 = -4 (since -3.5 floors to -4), and `%` is defined to satisfy `a == (a // b) * b + a % b`, which forces the remainder to take the sign of the divisor, giving -7 % 2 = 1. JavaScript's `/` followed by `Math.trunc` (or the legacy `~~`/`|0` truncation idioms) rounds toward zero instead, so -7/2 truncates to -3, and `%` in JS is a remainder operation (not a true modulo) that takes the sign of the DIVIDEND, giving -7 % 2 = -1. (JavaScript's `/` itself returns -3.5; `Math.floor(-7 / 2)` gives -4 and matches Python's `//`, so the quotient only diverges if you truncate. The `%` sign difference is built into the operator.)",
|
|
12
|
-
"affects": [
|
|
13
|
-
"python",
|
|
14
|
-
"javascript"
|
|
15
|
-
],
|
|
16
|
-
"repros": [
|
|
17
|
-
{
|
|
18
|
-
"language": "python",
|
|
19
|
-
"code": "print(-7 // 2, -7 % 2)\nprint(7 // -2, 7 % -2)\n",
|
|
20
|
-
"expected_output": "-4 1\n-4 -1",
|
|
21
|
-
"verified_with": "Python 3.14.2"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"language": "javascript",
|
|
25
|
-
"code": "console.log(Math.trunc(-7 / 2), -7 % 2);\nconsole.log(Math.trunc(7 / -2), 7 % -2);\n// A true floor-mod, matching Python, has to be written out explicitly:\nfunction floorMod(a, b) { return ((a % b) + b) % b; }\nconsole.log(floorMod(-7, 2));\n",
|
|
26
|
-
"expected_output": "-3 -1\n-3 1\n1",
|
|
27
|
-
"verified_with": "Node.js v22.22.2"
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"fix": "Never port a division/modulo-based formula between Python and JavaScript without checking the sign convention explicitly; if you need Python's floor-mod behavior in JS, write it out as `((a % b) + b) % b` rather than relying on `%` alone.",
|
|
31
|
-
"related": [],
|
|
32
|
-
"sources": [
|
|
33
|
-
{
|
|
34
|
-
"title": "Python docs: binary arithmetic operations (floor division)",
|
|
35
|
-
"url": "https://docs.python.org/3/reference/expressions.html#binary-arithmetic-operations"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"title": "ECMA-262: Remainder operator semantics",
|
|
39
|
-
"url": "https://tc39.es/ecma262/#sec-numeric-types-number-remainder"
|
|
40
|
-
}
|
|
41
|
-
],
|
|
42
|
-
"tags": [
|
|
43
|
-
"validation",
|
|
44
|
-
"pagination",
|
|
45
|
-
"database"
|
|
46
|
-
],
|
|
47
|
-
"test_cases": []
|
|
48
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "json-parse-loses-id-precision",
|
|
3
|
-
"title": "JSON.parse silently rounds a 64-bit id to the nearest value a JS number can hold",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "Parsing a JSON number and then stringifying it again gives back the exact same digits you started with.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "JSON.parse(\"9007199254740993\") (a Twitter/Discord/Snowflake-style 64-bit id, one more than Number.MAX_SAFE_INTEGER)",
|
|
8
|
-
"description": "Any API that returns large integer ids as bare JSON numbers (not strings) -- Twitter/X status ids, Discord snowflakes, some database auto-increment ids -- consumed by a JavaScript client."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "An id of 9007199254740993 round-trips through `JSON.parse` as 9007199254740992 -- a different, smaller, EVEN number -- so looking that id up, comparing it, or sending it back to the server can silently hit the wrong record or none at all.",
|
|
11
|
-
"why": "The JSON specification defines numbers as an abstract decimal value with no size limit, but JavaScript's `JSON.parse` converts every JSON number into a 64-bit double, which (per IEEE 754) represents every integer exactly only up to 2^53; above that, neighbouring integers start sharing a double, which is why 2^53 - 1 is the largest 'safe' integer. 9007199254740993 (2^53 + 1) has no exact double representation; the nearest one is 2^53 (9007199254740992), and ties-to-even rounding picks that over 2^53 + 2. This is exactly why large-id APIs increasingly send such ids as JSON strings instead of bare numbers.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "javascript",
|
|
18
|
-
"code": "const id = JSON.parse(\"9007199254740993\");\nconsole.log(id);\nconsole.log(id === 9007199254740992);\nconsole.log(JSON.stringify(id));\n",
|
|
19
|
-
"expected_output": "9007199254740992\ntrue\n9007199254740992",
|
|
20
|
-
"verified_with": "Node.js v22.22.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "python",
|
|
24
|
-
"code": "import json\n\n# Python's json module parses integers into arbitrary-precision int, so no\n# precision is lost here -- the gotcha is specific to a 64-bit-double parser.\nid_value = json.loads(\"9007199254740993\")\nprint(id_value, id_value == 9007199254740993)\n",
|
|
25
|
-
"expected_output": "9007199254740993 True",
|
|
26
|
-
"verified_with": "Python 3.14.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Never transmit an id that could exceed 2^53 - 1 as a bare JSON number; encode it as a JSON string on the wire, and parse/compare it as a string or BigInt on the JavaScript side, never as a plain `number`. If you cannot change the wire format, V8-based runtimes (Node 22, recent Chrome) pass the raw source text to a reviver as a third argument: `JSON.parse(text, (k, v, ctx) => typeof v === 'number' && !Number.isSafeInteger(v) ? BigInt(ctx.source) : v)`.",
|
|
30
|
-
"related": [
|
|
31
|
-
"max-safe-integer-plus-one-equals-plus-two"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "RFC 8259: The JSON Data Interchange Format (numbers have no size limit)",
|
|
36
|
-
"url": "https://www.rfc-editor.org/rfc/rfc8259#section-6"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"title": "MDN: JSON.parse",
|
|
40
|
-
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse"
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"tags": [
|
|
44
|
-
"id",
|
|
45
|
-
"json",
|
|
46
|
-
"parsing",
|
|
47
|
-
"database"
|
|
48
|
-
],
|
|
49
|
-
"test_cases": [
|
|
50
|
-
{
|
|
51
|
-
"inputs": [
|
|
52
|
-
"9007199254740993"
|
|
53
|
-
],
|
|
54
|
-
"relation": "no_crash",
|
|
55
|
-
"note": "Parsing a large id from JSON must not silently change its value; compare as string or BigInt."
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "max-safe-integer-plus-one-equals-plus-two",
|
|
3
|
-
"title": "Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "Adding 1 to a number and adding 2 to the same number always give different results.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 -- both equal 9007199254740992",
|
|
8
|
-
"description": "An auto-incrementing counter, row id, or 'view count' stored as a plain JS number that has climbed past 2^53."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "A counter that increments a plain JS `number` past 2^53 - 1 stops being able to tell consecutive values apart: incrementing by 1 does nothing some of the time, so a counter can appear to stall or two different logical events can be recorded with the identical numeric id.",
|
|
11
|
-
"why": "JS numbers are IEEE 754 double-precision floats, which have 53 bits of mantissa and so they represent every integer exactly only up to 2^53. `Number.MAX_SAFE_INTEGER` is 2^53 - 1 because it is the largest n for which n and n + 1 are both exact. Between 2^53 and 2^54 the representable doubles are 2 apart, so `MAX_SAFE_INTEGER + 1` is exactly 2^53, and `MAX_SAFE_INTEGER + 2` (2^53 + 1) sits halfway between 2^53 and 2^53 + 2 and rounds, ties-to-even, down to 2^53 as well.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "javascript",
|
|
18
|
-
"code": "console.log(Number.MAX_SAFE_INTEGER);\nconsole.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);\nconsole.log(Number.MAX_SAFE_INTEGER + 1, Number.MAX_SAFE_INTEGER + 2);\nconsole.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1));\n",
|
|
19
|
-
"expected_output": "9007199254740991\ntrue\n9007199254740992 9007199254740992\nfalse",
|
|
20
|
-
"verified_with": "Node.js v22.22.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "python",
|
|
24
|
-
"code": "# Python ints are arbitrary precision, so the same arithmetic never collides.\nmax_safe = 2**53 - 1\nprint(max_safe + 1 == max_safe + 2)\nprint(max_safe + 1, max_safe + 2)\n",
|
|
25
|
-
"expected_output": "False\n9007199254740992 9007199254740993",
|
|
26
|
-
"verified_with": "Python 3.14.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Use `BigInt` (or a string) for any counter, id, or accumulated value that could exceed 2^53 - 1 in JavaScript; check `Number.isSafeInteger()` before trusting equality/ordering on a large computed number.",
|
|
30
|
-
"related": [
|
|
31
|
-
"json-parse-loses-id-precision"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "MDN: Number.MAX_SAFE_INTEGER",
|
|
36
|
-
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"title": "IEEE 754-2019 (floating-point arithmetic)",
|
|
40
|
-
"url": "https://ieeexplore.ieee.org/document/8766229"
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"tags": [
|
|
44
|
-
"id",
|
|
45
|
-
"counter",
|
|
46
|
-
"database",
|
|
47
|
-
"validation"
|
|
48
|
-
],
|
|
49
|
-
"test_cases": [
|
|
50
|
-
{
|
|
51
|
-
"inputs": [
|
|
52
|
-
9007199254740991
|
|
53
|
-
],
|
|
54
|
-
"relation": "no_crash",
|
|
55
|
-
"note": "A safe-integer check should flag this value's successor as unsafe, not silently accept it."
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "number-empty-and-whitespace-string-equals-zero",
|
|
3
|
-
"title": "Number(\"\") and Number(\" \") both equal 0",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "An empty or blank string can't be converted to a valid number -- it should fail or return NaN.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "Number(\"\") === 0 and Number(\" \") === 0",
|
|
8
|
-
"description": "Converting a form field or query-string parameter that the user left blank, using `Number(value)` as a validation/conversion step."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "A blank form field, passed through `Number()` as part of 'sanitize to a number', becomes the perfectly valid number 0 instead of failing validation, so an unanswered quantity/price/age field silently records as zero rather than being rejected as missing.",
|
|
11
|
-
"why": "The ECMAScript string-to-number grammar defines the empty string (and a string containing only whitespace, since leading/trailing whitespace is trimmed first) as a valid `StringNumericLiteral` whose value is 0 -- this is an explicit rule in the spec, not an accidental byproduct of trimming. `parseInt` and `parseFloat`, by contrast, both return `NaN` for the same inputs, because they don't follow this grammar rule.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "javascript",
|
|
18
|
-
"code": "console.log(Number(\"\"), Number(\" \"), Number(\"\\t\\n\"));\nconsole.log(parseInt(\"\"), parseFloat(\" \"));\n",
|
|
19
|
-
"expected_output": "0 0 0\nNaN NaN",
|
|
20
|
-
"verified_with": "Node.js v22.22.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "python",
|
|
24
|
-
"code": "# Python's numeric constructors raise on an empty or blank string rather than defaulting to 0.\nfor s in (\"\", \" \"):\n try:\n int(s)\n except ValueError as e:\n print(repr(s), \"-> ValueError:\", e)\n",
|
|
25
|
-
"expected_output": "'' -> ValueError: invalid literal for int() with base 10: ''\n' ' -> ValueError: invalid literal for int() with base 10: ' '",
|
|
26
|
-
"verified_with": "Python 3.14.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Check for an empty/whitespace-only string explicitly before calling `Number()` on user input; don't rely on it to reject blanks, since by spec it treats them as a valid zero.",
|
|
30
|
-
"related": [],
|
|
31
|
-
"sources": [
|
|
32
|
-
{
|
|
33
|
-
"title": "ECMA-262: ToNumber applied to the String type (StringNumericLiteral)",
|
|
34
|
-
"url": "https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"title": "MDN: Number()",
|
|
38
|
-
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"
|
|
39
|
-
}
|
|
40
|
-
],
|
|
41
|
-
"tags": [
|
|
42
|
-
"parsing",
|
|
43
|
-
"validation",
|
|
44
|
-
"forms"
|
|
45
|
-
],
|
|
46
|
-
"test_cases": [
|
|
47
|
-
{
|
|
48
|
-
"inputs": [
|
|
49
|
-
""
|
|
50
|
-
],
|
|
51
|
-
"relation": "no_crash",
|
|
52
|
-
"note": "A numeric-field validator must treat an empty string as missing, not silently accept it as zero."
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "parseint-null-radix36-digit-soup",
|
|
3
|
-
"title": "parseInt(null, 36) returns 1112745",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "Passing null where a string is expected is harmless, or at worst gives you 0 or NaN.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "parseInt(null, 36) === 1112745",
|
|
8
|
-
"description": "A missing or unset form field, config value, or optional function argument that falls through to `parseInt` without a guard, combined with a non-default radix such as base36 (used for compact id encoding)."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "A missing value that should produce 0, NaN, or an error instead silently produces a real-looking, nonzero integer -- 1112745 -- which can pass a `typeof x === 'number' && !isNaN(x)` guard and flow deep into the system before anyone notices the input was never actually a number.",
|
|
11
|
-
"why": "`parseInt`'s first argument is coerced to a string before parsing, and `String(null)` is the literal string `\"null\"`. In base 36, digits run 0-9 then a-z representing 10-35, so every character of \"null\" (n, u, l, l) is a valid base-36 digit, and the whole string parses successfully as a number -- specifically 1112745. Nothing about this path raises an error or returns NaN, because from `parseInt`'s point of view it was simply handed a valid base-36 numeral.",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "javascript",
|
|
18
|
-
"code": "console.log(String(null));\nconsole.log(parseInt(null, 36));\nconsole.log(parseInt(\"null\", 36));\nconsole.log(parseInt(undefined, 36)); // String(undefined) === 'undefined', also all valid base-36 digits\n",
|
|
19
|
-
"expected_output": "null\n1112745\n1112745\n86464843759093",
|
|
20
|
-
"verified_with": "Node.js v22.22.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "python",
|
|
24
|
-
"code": "# Python's int() with an explicit base requires an actual string and rejects None outright.\ntry:\n int(None, 36)\nexcept TypeError as e:\n print(\"TypeError:\", e)\n",
|
|
25
|
-
"expected_output": "TypeError: int() can't convert non-string with explicit base",
|
|
26
|
-
"verified_with": "Python 3.14.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Guard against `null`/`undefined` before calling `parseInt` rather than relying on it to fail loudly; it won't, for any radix where the stringified sentinel's letters happen to be valid digits.",
|
|
30
|
-
"related": [
|
|
31
|
-
"parseint-stops-at-first-invalid-char-vs-number"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "MDN: parseInt()",
|
|
36
|
-
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"title": "ECMA-262: parseInt (string coercion of the first argument)",
|
|
40
|
-
"url": "https://tc39.es/ecma262/#sec-parseint-string-radix"
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"tags": [
|
|
44
|
-
"parsing",
|
|
45
|
-
"validation",
|
|
46
|
-
"forms"
|
|
47
|
-
],
|
|
48
|
-
"test_cases": [
|
|
49
|
-
{
|
|
50
|
-
"inputs": [
|
|
51
|
-
null
|
|
52
|
-
],
|
|
53
|
-
"relation": "no_crash",
|
|
54
|
-
"note": "A function wrapping parseInt with a non-10 radix should explicitly reject null/undefined rather than let String() coercion through."
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "parseint-number-argument-scientific-notation",
|
|
3
|
-
"title": "parseInt(0.0000005) returns 5, not 0",
|
|
4
|
-
"category": "integers",
|
|
5
|
-
"assumption": "parseInt() reads the digits of the number you gave it, in the order you wrote them.",
|
|
6
|
-
"input": {
|
|
7
|
-
"display": "parseInt(0.0000005) -- note: the NUMBER 0.0000005, not the string \"0.0000005\"",
|
|
8
|
-
"description": "Calling parseInt on a small decimal number directly (rather than on a string), e.g. while sanitizing a computed fractional value into an integer."
|
|
9
|
-
},
|
|
10
|
-
"what_goes_wrong": "`parseInt(0.0000005)` returns 5 -- not 0, and not an error -- because the number gets stringified to scientific notation before parseInt ever sees a digit of the original decimal.",
|
|
11
|
-
"why": "`parseInt` only accepts a string; when given a `number`, JavaScript first coerces it to a string via the Number-to-String algorithm. That algorithm switches to exponent notation for any magnitude below 1e-6 (and at or above 1e21), so 0.000001 stringifies as \"0.000001\" but 0.0000005 becomes \"5e-7\", not \"0.0000005\". `parseInt` then reads the string from the start, parsing digits until it hits the first character that isn't part of an integer -- which is \"e\" -- so it returns 5, the only digit it saw before stopping. Calling `parseInt(\"0.0000005\")` on the literal string instead returns 0, because the string parse stops at the \".\". The large end bites the same way: `parseInt(1e21)` is 1, because 1e21 stringifies as \"1e+21\".",
|
|
12
|
-
"affects": [
|
|
13
|
-
"javascript"
|
|
14
|
-
],
|
|
15
|
-
"repros": [
|
|
16
|
-
{
|
|
17
|
-
"language": "javascript",
|
|
18
|
-
"code": "console.log((0.0000005).toString()); // what parseInt actually sees\nconsole.log(parseInt(0.0000005)); // number argument -> stringified first\nconsole.log(parseInt(\"0.0000005\")); // string argument -> no scientific notation\n",
|
|
19
|
-
"expected_output": "5e-7\n5\n0",
|
|
20
|
-
"verified_with": "Node.js v22.22.2"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"language": "python",
|
|
24
|
-
"code": "# Python's int() refuses a float string outright rather than guessing a prefix.\ntry:\n int(\"0.0000005\")\nexcept ValueError as e:\n print(\"ValueError:\", e)\nprint(int(0.0000005)) # int() of an actual float truncates toward zero instead\n",
|
|
25
|
-
"expected_output": "ValueError: invalid literal for int() with base 10: '0.0000005'\n0",
|
|
26
|
-
"verified_with": "Python 3.14.2"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"fix": "Never pass a `number` directly to `parseInt`; convert deliberately with `Math.trunc()`/`Math.floor()` for numbers, and reserve `parseInt` for strings whose format you've already validated.",
|
|
30
|
-
"related": [
|
|
31
|
-
"parseint-stops-at-first-invalid-char-vs-number"
|
|
32
|
-
],
|
|
33
|
-
"sources": [
|
|
34
|
-
{
|
|
35
|
-
"title": "MDN: parseInt()",
|
|
36
|
-
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"title": "ECMA-262: Number::toString",
|
|
40
|
-
"url": "https://tc39.es/ecma262/#sec-numeric-types-number-tostring"
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"tags": [
|
|
44
|
-
"parsing",
|
|
45
|
-
"validation"
|
|
46
|
-
],
|
|
47
|
-
"test_cases": []
|
|
48
|
-
}
|