-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify_json.mjs
More file actions
208 lines (184 loc) · 5.87 KB
/
verify_json.mjs
File metadata and controls
208 lines (184 loc) · 5.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/**
* Verification Script for Locale JSON Files
*
* Purpose:
* 1. Checks for duplicate keys in the locale JSON files.
* 2. Validates that the locale files are valid JSON.
*
* Usage: npm run verify-locales
*/
import fs from 'fs';
import path from 'path';
// Dynamically discover all .json files in src/locales/ and src/locales-extension/
const targetDirs = ['src/locales', 'src/locales-extension'];
const files = targetDirs.flatMap(dir => {
const dirPath = path.join(process.cwd(), dir);
if (!fs.existsSync(dirPath)) return [];
return fs.readdirSync(dirPath)
.filter(f => f.endsWith('.json'))
.map(f => path.join(dir, f));
});
let hasError = false;
// Regex to match valid JSON numbers (see ECMA-404)
const JSON_NUMBER_REGEX = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/;
/**
* Lightweight JSON parser to detect duplicate keys per object while still
* validating that the JSON is well-formed. Avoids false positives from
* sibling objects using the same property names.
*/
function verifyJsonStructure(source) {
let index = 0;
const duplicates = [];
function error(message) {
throw new Error(`${message} at position ${index}`);
}
function currentChar() {
return source[index];
}
function advance(step = 1) {
index += step;
}
function skipWhitespace() {
while (/\s/.test(currentChar())) advance();
}
function parseString() {
if (currentChar() !== '"') error('Expected string');
const start = index;
advance(); // skip opening quote
while (index < source.length) {
const ch = currentChar();
if (ch === '\\') {
advance(2); // skip escaped char
continue;
}
if (ch === '"') {
const raw = source.slice(start, index + 1);
try {
// Use JSON.parse to properly unescape
JSON.parse(raw);
} catch (e) {
const stringPos = index - start;
error(`Invalid string escape sequence in ${raw}: ${e?.message || e} (at position ${stringPos} within the string, from JSON position ${start} to ${index})`);
}
advance(); // closing quote
return raw;
}
advance();
}
error('Unterminated string');
}
function parseNumber() {
const remaining = source.slice(index);
const match = remaining.match(JSON_NUMBER_REGEX);
if (!match) error('Invalid number');
advance(match[0].length);
}
function parseLiteral(expected) {
if (source.slice(index, index + expected.length) !== expected) {
error(`Expected ${expected}`);
}
advance(expected.length);
}
function parseArray() {
advance(); // skip [
skipWhitespace();
if (currentChar() === ']') {
advance();
return;
}
while (true) {
parseValue();
skipWhitespace();
const ch = currentChar();
if (ch === ',') {
advance();
skipWhitespace();
continue;
}
if (ch === ']') {
advance();
return;
}
error('Expected , or ] in array');
}
}
function parseObject() {
advance(); // skip {
skipWhitespace();
const seenKeys = new Set();
if (currentChar() === '}') {
advance();
return;
}
while (true) {
skipWhitespace();
const rawKey = parseString();
const key = JSON.parse(rawKey);
skipWhitespace();
if (currentChar() !== ':') error('Expected : after key');
advance();
skipWhitespace();
if (seenKeys.has(key)) {
duplicates.push(key);
} else {
seenKeys.add(key);
}
parseValue();
skipWhitespace();
const ch = currentChar();
if (ch === ',') {
advance();
skipWhitespace();
continue;
}
if (ch === '}') {
advance();
return;
}
error('Expected , or } in object');
}
}
function parseValue() {
skipWhitespace();
const ch = currentChar();
if (ch === '"') return parseString();
if (ch === '{') return parseObject();
if (ch === '[') return parseArray();
if (ch === '-' || (ch >= '0' && ch <= '9')) return parseNumber();
if (ch === 't') return parseLiteral('true');
if (ch === 'f') return parseLiteral('false');
if (ch === 'n') return parseLiteral('null');
error('Unexpected character');
}
parseValue();
skipWhitespace();
if (index !== source.length) {
error('Unexpected trailing content');
}
return duplicates;
}
files.forEach(file => {
const filePath = path.resolve(process.cwd(), file);
console.log(`Checking ${file}...`);
try {
const content = fs.readFileSync(filePath, 'utf8');
// Validate structure and detect duplicates per object
const duplicates = verifyJsonStructure(content);
if (duplicates.length > 0) {
console.error(`❌ Found duplicate keys in ${file}:`, [...new Set(duplicates)]);
hasError = true;
} else {
console.log(`✅ No duplicate keys found in ${file}.`);
}
console.log(`✅ Valid JSON format: ${file}`);
} catch (err) {
console.error(`❌ Error processing ${file}:`, err.message);
hasError = true;
}
console.log('-----------------------------------');
});
if (hasError) {
process.exit(1);
} else {
console.log('All checks passed!');
}