|
1 | 1 | import {
|
2 | 2 | isArgumentElement,
|
3 | 3 | isDateElement,
|
4 |
| - isLiteralElement, |
5 | 4 | isNumberElement,
|
6 | 5 | isPluralElement,
|
7 |
| - isPoundElement, |
8 | 6 | isSelectElement,
|
9 | 7 | isTagElement,
|
10 | 8 | isTimeElement,
|
11 | 9 | MessageFormatElement,
|
12 | 10 | PluralElement,
|
13 | 11 | PluralOrSelectOption,
|
14 | 12 | SelectElement,
|
| 13 | + TYPE, |
15 | 14 | } from './types'
|
16 | 15 |
|
17 | 16 | function cloneDeep<T>(obj: T): T {
|
@@ -101,122 +100,63 @@ export function hoistSelectors(
|
101 | 100 | return ast
|
102 | 101 | }
|
103 | 102 |
|
104 |
| -function isStructurallySamePluralOrSelect( |
105 |
| - el1: PluralElement | SelectElement, |
106 |
| - el2: PluralElement | SelectElement |
107 |
| -): boolean { |
108 |
| - const options1 = el1.options |
109 |
| - const options2 = el2.options |
110 |
| - if (Object.keys(options1).length !== Object.keys(options2).length) { |
111 |
| - return false |
112 |
| - } |
113 |
| - for (const key in options1) { |
114 |
| - if (!options2[key]) { |
115 |
| - return false |
| 103 | +/** |
| 104 | + * Collect all variables in an AST to Record<string, TYPE> |
| 105 | + * @param ast AST to collect variables from |
| 106 | + * @param vars Record of variable name to variable type |
| 107 | + */ |
| 108 | +function collectVariables( |
| 109 | + ast: MessageFormatElement[], |
| 110 | + vars: Map<string, TYPE> = new Map<string, TYPE>() |
| 111 | +): void { |
| 112 | + ast.forEach(el => { |
| 113 | + if ( |
| 114 | + isArgumentElement(el) || |
| 115 | + isDateElement(el) || |
| 116 | + isTimeElement(el) || |
| 117 | + isNumberElement(el) |
| 118 | + ) { |
| 119 | + if (el.value in vars && vars.get(el.value) !== el.type) { |
| 120 | + throw new Error(`Variable ${el.value} has conflicting types`) |
| 121 | + } |
| 122 | + vars.set(el.value, el.type) |
116 | 123 | }
|
117 |
| - if (!isStructurallySame(options1[key].value, options2[key].value)) { |
118 |
| - return false |
| 124 | + |
| 125 | + if (isPluralElement(el) || isSelectElement(el)) { |
| 126 | + vars.set(el.value, el.type) |
| 127 | + Object.keys(el.options).forEach(k => { |
| 128 | + collectVariables(el.options[k].value, vars) |
| 129 | + }) |
119 | 130 | }
|
120 |
| - } |
121 |
| - return true |
| 131 | + |
| 132 | + if (isTagElement(el)) { |
| 133 | + vars.set(el.value, el.type) |
| 134 | + collectVariables(el.children, vars) |
| 135 | + } |
| 136 | + }) |
122 | 137 | }
|
123 | 138 |
|
| 139 | +/** |
| 140 | + * Check if 2 ASTs are structurally the same. This primarily means that |
| 141 | + * they have the same variables with the same type |
| 142 | + * @param a |
| 143 | + * @param b |
| 144 | + * @returns |
| 145 | + */ |
124 | 146 | export function isStructurallySame(
|
125 | 147 | a: MessageFormatElement[],
|
126 | 148 | b: MessageFormatElement[]
|
127 | 149 | ): boolean {
|
128 |
| - const aWithoutLiteral = a.filter(el => !isLiteralElement(el)) |
129 |
| - const bWithoutLiteral = b.filter(el => !isLiteralElement(el)) |
130 |
| - if (aWithoutLiteral.length !== bWithoutLiteral.length) { |
| 150 | + const aVars = new Map<string, TYPE>() |
| 151 | + const bVars = new Map<string, TYPE>() |
| 152 | + collectVariables(a, aVars) |
| 153 | + collectVariables(b, bVars) |
| 154 | + |
| 155 | + if (aVars.size !== bVars.size) { |
131 | 156 | return false
|
132 | 157 | }
|
133 | 158 |
|
134 |
| - const elementsMapInA = aWithoutLiteral.reduce< |
135 |
| - Record<string, MessageFormatElement> |
136 |
| - >((all, el) => { |
137 |
| - if (isPoundElement(el)) { |
138 |
| - all['#'] = el |
139 |
| - return all |
140 |
| - } |
141 |
| - all[el.value] = el |
142 |
| - return all |
143 |
| - }, {}) |
144 |
| - |
145 |
| - const elementsMapInB = bWithoutLiteral.reduce< |
146 |
| - Record<string, MessageFormatElement> |
147 |
| - >((all, el) => { |
148 |
| - if (isPoundElement(el)) { |
149 |
| - all['#'] = el |
150 |
| - return all |
151 |
| - } |
152 |
| - all[el.value] = el |
153 |
| - return all |
154 |
| - }, {}) |
155 |
| - |
156 |
| - for (const varName of Object.keys(elementsMapInA)) { |
157 |
| - const elA = elementsMapInA[varName] |
158 |
| - const elB = elementsMapInB[varName] |
159 |
| - if (!elB) { |
160 |
| - return false |
161 |
| - } |
162 |
| - |
163 |
| - if (elA.type !== elB.type) { |
164 |
| - return false |
165 |
| - } |
166 |
| - |
167 |
| - if (isLiteralElement(elA) || isLiteralElement(elB)) { |
168 |
| - continue |
169 |
| - } |
170 |
| - |
171 |
| - if ( |
172 |
| - isArgumentElement(elA) && |
173 |
| - isArgumentElement(elB) && |
174 |
| - elA.value !== elB.value |
175 |
| - ) { |
176 |
| - return false |
177 |
| - } |
178 |
| - |
179 |
| - if (isPoundElement(elA) || isPoundElement(elB)) { |
180 |
| - continue |
181 |
| - } |
182 |
| - |
183 |
| - if ( |
184 |
| - isDateElement(elA) || |
185 |
| - isTimeElement(elA) || |
186 |
| - isNumberElement(elA) || |
187 |
| - isDateElement(elB) || |
188 |
| - isTimeElement(elB) || |
189 |
| - isNumberElement(elB) |
190 |
| - ) { |
191 |
| - if (elA.value !== elB.value) { |
192 |
| - return false |
193 |
| - } |
194 |
| - } |
195 |
| - |
196 |
| - if ( |
197 |
| - isPluralElement(elA) && |
198 |
| - isPluralElement(elB) && |
199 |
| - !isStructurallySamePluralOrSelect(elA, elB) |
200 |
| - ) { |
201 |
| - return false |
202 |
| - } |
203 |
| - |
204 |
| - if ( |
205 |
| - isSelectElement(elA) && |
206 |
| - isSelectElement(elB) && |
207 |
| - isStructurallySamePluralOrSelect(elA, elB) |
208 |
| - ) { |
209 |
| - return false |
210 |
| - } |
211 |
| - |
212 |
| - if (isTagElement(elA) && isTagElement(elB)) { |
213 |
| - if (elA.value !== elB.value) { |
214 |
| - return false |
215 |
| - } |
216 |
| - if (!isStructurallySame(elA.children, elB.children)) { |
217 |
| - return false |
218 |
| - } |
219 |
| - } |
220 |
| - } |
221 |
| - return true |
| 159 | + return Array.from(aVars.entries()).every(([key, type]) => { |
| 160 | + return bVars.has(key) && bVars.get(key) === type |
| 161 | + }) |
222 | 162 | }
|
0 commit comments