diff --git a/test/util-file.test.js b/test/util-file.test.js index e66a04e..cc2ab21 100644 --- a/test/util-file.test.js +++ b/test/util-file.test.js @@ -402,6 +402,21 @@ describe('openapi-format CLI file tests', () => { expect(result).toEqual({name: 'John', age: 30}); }); + it('should keep Number.MAX_SAFE_INTEGER as a number in YAML', async () => { + const result = await parseString(`value: ${Number.MAX_SAFE_INTEGER}`); + expect(result).toEqual({value: Number.MAX_SAFE_INTEGER}); + }); + + it('should keep Number.MIN_SAFE_INTEGER as a number in YAML', async () => { + const result = await parseString(`value: ${Number.MIN_SAFE_INTEGER}`); + expect(result).toEqual({value: Number.MIN_SAFE_INTEGER}); + }); + + it('should encode YAML integers beyond the safe integer range', async () => { + const result = await parseString('positive: 9007199254740993\nnegative: -9007199254740993'); + expect(result).toEqual({positive: '9007199254740993===', negative: '-9007199254740993==='}); + }); + it('should preserve quoted JSON examples with high-precision numbers and following schemas', async () => { const yamlString = `openapi: 3.1.0 info: @@ -749,6 +764,30 @@ components: const output = encodeLargeNumbers(input); expect(output).toBe(input); }); + + test('should not encode Number.MAX_SAFE_INTEGER', () => { + const input = `key: ${Number.MAX_SAFE_INTEGER}\n`; + const output = encodeLargeNumbers(input); + expect(output).toBe('key: 9007199254740991\n'); + }); + + test('should not encode Number.MIN_SAFE_INTEGER', () => { + const input = `key: ${Number.MIN_SAFE_INTEGER}\n`; + const output = encodeLargeNumbers(input); + expect(output).toBe('key: -9007199254740991\n'); + }); + + test('should not encode a 16 digit integer that fits in a double', () => { + const input = 'key: 1234567890123456\n'; + const output = encodeLargeNumbers(input); + expect(output).toBe('key: 1234567890123456\n'); + }); + + test('should encode an integer beyond Number.MAX_SAFE_INTEGER', () => { + const input = 'key: 9007199254740993\n'; + const output = encodeLargeNumbers(input); + expect(output).toBe('key: "9007199254740993==="\n'); + }); }); describe('addQuotesToRefInString function', () => { diff --git a/utils/file.js b/utils/file.js index 3df9bd6..fa0fb4c 100644 --- a/utils/file.js +++ b/utils/file.js @@ -497,6 +497,24 @@ async function getRemoteFile(filePath) { return inputContent; } +/** + * Check whether a numeric literal survives a round-trip through `Number(...)` and + * `Number#toString()`. + * Short literals (<= 15 digits) are treated as safe; longer ones are considered unsafe + * when parsing/stringifying changes the literal or produces exponential notation. + * @param {string} source - The raw numeric literal. + * @returns {boolean} True when the literal would not round-trip as the same string. + */ +function isUnsafeNumberLiteral(source) { + const parsed = Number(source).toString(); + if (parsed.includes('e')) return true; + + const digitCount = source.replace(/[^0-9]/g, '').length; + if (digitCount <= 15) return false; + + return parsed !== source; +} + /** * Convert large number value safely before parsing * @param inputContent Input content. @@ -510,7 +528,7 @@ function encodeLargeNumbers(inputContent) { const rgx = new RegExp(endChar, 'g'); const number = rawInput.replace(/: /g, '').replace(rgx, ''); // Handle large numbers safely in javascript - if (Number(number).toString().includes('e') || number.replace('.', '').length > 15) { + if (isUnsafeNumberLiteral(number)) { return `: "${number}==="${endChar}`; } else { return `: ${number}${endChar}`; @@ -535,8 +553,7 @@ function encodeLargeNumberScalars(doc) { } const source = value.source; - const digitCount = source.replace(/[^0-9]/g, '').length; - if (Number(source).toString().includes('e') || digitCount > 15) { + if (isUnsafeNumberLiteral(source)) { value.value = `${source}===`; value.type = 'QUOTE_DOUBLE'; }