Skip to content

Commit 358750c

Browse files
committed
Add support for numeric keys
1 parent 2915c15 commit 358750c

File tree

3 files changed

+48
-11
lines changed

3 files changed

+48
-11
lines changed

src/json/__tests__/fixtures/01-float.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
objectVersion = 54;
55
objects = {
6-
83CBB9F71A601CBA00E9B192 = {
6+
123456789123456789012345 = {
77
isa = PBXProject;
88
one = 0.0;
99
two = 1.1;
@@ -13,5 +13,5 @@
1313
six = 01;
1414
};
1515
};
16-
rootObject = 83CBB9F71A601CBA00E9B192;
16+
rootObject = 123456789123456789012345;
1717
}

src/json/__tests__/json.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@ import {
1010
} from "./compare-utils";
1111

1212
describe(parse, () => {
13+
it("should keep numeric object keys as strings", () => {
14+
const input = `{ 123 = abc; 456 = { 789 = def; }; }`;
15+
const result = parse(input) as any;
16+
17+
expect(result).toEqual({
18+
"123": "abc",
19+
"456": {
20+
"789": "def"
21+
}
22+
});
23+
24+
// Verify keys are strings, not numbers
25+
expect(typeof Object.keys(result)[0]).toBe("string");
26+
expect(typeof Object.keys(result)[1]).toBe("string");
27+
expect(typeof Object.keys(result["456"])[0]).toBe("string");
28+
});
29+
1330
const fixtures = [
1431
"01-float.pbxproj",
1532
"006-spm.pbxproj",

src/json/visitor/JsonVisitor.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,25 @@ export class JsonVisitor extends BaseVisitor {
3737
}
3838

3939
objectItem(ctx: any) {
40+
// Object keys must always be strings, even if they're numeric
41+
const key = this.visitIdentifierAsString(ctx.identifier);
4042
return {
41-
[this.visit(ctx.identifier)]: this.visit(ctx.value),
43+
[key]: this.visit(ctx.value),
4244
};
4345
}
4446

47+
/** Visit an identifier and ensure the result is always a string (used for object keys) */
48+
visitIdentifierAsString(identifierCtx: any) {
49+
// Extract the actual context - identifierCtx is an array with a single item containing children
50+
const ctx = identifierCtx[0]?.children || identifierCtx;
51+
if (ctx.QuotedString) {
52+
return ctx.QuotedString[0].payload ?? ctx.QuotedString[0].image;
53+
} else if (ctx.StringLiteral) {
54+
return ctx.StringLiteral[0].payload ?? ctx.StringLiteral[0].image;
55+
}
56+
throw new Error("unhandled identifier: " + JSON.stringify(identifierCtx));
57+
}
58+
4559
identifier(ctx: any) {
4660
if (ctx.QuotedString) {
4761
return ctx.QuotedString[0].payload ?? ctx.QuotedString[0].image;
@@ -71,7 +85,19 @@ function parseType(literal: string): number | string {
7185
if (/^0\d+$/.test(literal)) {
7286
return literal;
7387
}
74-
88+
89+
// Handle integers - check if they're safe to convert
90+
if (/^\d+$/.test(literal)) {
91+
const num = parseInt(literal, 10);
92+
// Only convert to number if it's within JavaScript's safe integer range
93+
// Xcode UUIDs are often 24 characters which exceed MAX_SAFE_INTEGER
94+
if (!isNaN(num) && Number.isSafeInteger(num)) {
95+
return num;
96+
}
97+
// Preserve as string if too large
98+
return literal;
99+
}
100+
75101
// Handle decimal numbers but preserve trailing zeros
76102
if (/^[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)$/.test(literal)) {
77103
if (/0$/.test(literal)) {
@@ -80,12 +106,6 @@ function parseType(literal: string): number | string {
80106
const num = parseFloat(literal);
81107
if (!isNaN(num)) return num;
82108
}
83-
84-
// Handle integers
85-
if (/^\d+$/.test(literal)) {
86-
const num = parseInt(literal, 10);
87-
if (!isNaN(num)) return num;
88-
}
89-
109+
90110
return literal;
91111
}

0 commit comments

Comments
 (0)