/
source_file_utils.ts
330 lines (301 loc) · 11.5 KB
/
source_file_utils.ts
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵParsedTranslation, ɵisMissingTranslationError, ɵmakeTemplateObject, ɵtranslate} from '@angular/localize';
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {Diagnostics} from '../../diagnostics';
/**
* Is the given `expression` the global `$localize` identifier?
*
* @param expression The expression to check.
* @param localizeName The configured name of `$localize`.
*/
export function isLocalize(
expression: NodePath, localizeName: string): expression is NodePath<t.Identifier> {
return isNamedIdentifier(expression, localizeName) && isGlobalIdentifier(expression);
}
/**
* Is the given `expression` an identifier with the correct `name`?
*
* @param expression The expression to check.
* @param name The name of the identifier we are looking for.
*/
export function isNamedIdentifier(
expression: NodePath, name: string): expression is NodePath<t.Identifier> {
return expression.isIdentifier() && expression.node.name === name;
}
/**
* Is the given `identifier` declared globally.
* @param identifier The identifier to check.
*/
export function isGlobalIdentifier(identifier: NodePath<t.Identifier>) {
return !identifier.scope || !identifier.scope.hasBinding(identifier.node.name);
}
/**
* Build a translated expression to replace the call to `$localize`.
* @param messageParts The static parts of the message.
* @param substitutions The expressions to substitute into the message.
*/
export function buildLocalizeReplacement(
messageParts: TemplateStringsArray, substitutions: readonly t.Expression[]): t.Expression {
let mappedString: t.Expression = t.stringLiteral(messageParts[0]);
for (let i = 1; i < messageParts.length; i++) {
mappedString =
t.binaryExpression('+', mappedString, wrapInParensIfNecessary(substitutions[i - 1]));
mappedString = t.binaryExpression('+', mappedString, t.stringLiteral(messageParts[i]));
}
return mappedString;
}
/**
* Extract the message parts from the given `call` (to `$localize`).
*
* The message parts will either by the first argument to the `call` or it will be wrapped in call
* to a helper function like `__makeTemplateObject`.
*
* @param call The AST node of the call to process.
*/
export function unwrapMessagePartsFromLocalizeCall(call: NodePath<t.CallExpression>):
TemplateStringsArray {
let cooked = call.get('arguments')[0];
if (cooked === undefined) {
throw new BabelParseError(call.node, '`$localize` called without any arguments.');
}
if (!cooked.isExpression()) {
throw new BabelParseError(
cooked.node, 'Unexpected argument to `$localize` (expected an array).');
}
// If there is no call to `__makeTemplateObject(...)`, then `raw` must be the same as `cooked`.
let raw = cooked;
// Check for cached call of the form `x || x = __makeTemplateObject(...)`
if (cooked.isLogicalExpression() && cooked.node.operator === '||' &&
cooked.get('left').isIdentifier()) {
const right = cooked.get('right');
if (right.isAssignmentExpression()) {
cooked = right.get('right');
if (!cooked.isExpression()) {
throw new BabelParseError(
cooked.node, 'Unexpected "makeTemplateObject()" function (expected an expression).');
}
}
}
// Check for `__makeTemplateObject(cooked, raw)` or `__templateObject()` calls.
if (cooked.isCallExpression()) {
let call = cooked;
if (call.get('arguments').length === 0) {
// No arguments so perhaps it is a `__templateObject()` call.
// Unwrap this to get the `_taggedTemplateLiteral(cooked, raw)` call.
call = unwrapLazyLoadHelperCall(call);
}
cooked = call.get('arguments')[0];
if (!cooked.isExpression()) {
throw new BabelParseError(
cooked.node,
'Unexpected `cooked` argument to the "makeTemplateObject()" function (expected an expression).');
}
const arg2 = call.get('arguments')[1];
if (arg2 && !arg2.isExpression()) {
throw new BabelParseError(
arg2.node,
'Unexpected `raw` argument to the "makeTemplateObject()" function (expected an expression).');
}
// If there is no second argument then assume that raw and cooked are the same
raw = arg2 !== undefined ? arg2 : cooked;
}
const cookedStrings = unwrapStringLiteralArray(cooked.node);
const rawStrings = unwrapStringLiteralArray(raw.node);
return ɵmakeTemplateObject(cookedStrings, rawStrings);
}
export function unwrapSubstitutionsFromLocalizeCall(call: t.CallExpression): t.Expression[] {
const expressions = call.arguments.splice(1);
if (!isArrayOfExpressions(expressions)) {
const badExpression = expressions.find(expression => !t.isExpression(expression)) !;
throw new BabelParseError(
badExpression,
'Invalid substitutions for `$localize` (expected all substitution arguments to be expressions).');
}
return expressions;
}
export function unwrapMessagePartsFromTemplateLiteral(elements: t.TemplateElement[]):
TemplateStringsArray {
const cooked = elements.map(q => {
if (q.value.cooked === undefined) {
throw new BabelParseError(
q, `Unexpected undefined message part in "${elements.map(q => q.value.cooked)}"`);
}
return q.value.cooked;
});
const raw = elements.map(q => q.value.raw);
return ɵmakeTemplateObject(cooked, raw);
}
/**
* Wrap the given `expression` in parentheses if it is a binary expression.
*
* This ensures that this expression is evaluated correctly if it is embedded in another expression.
*
* @param expression The expression to potentially wrap.
*/
export function wrapInParensIfNecessary(expression: t.Expression): t.Expression {
if (t.isBinaryExpression(expression)) {
return t.parenthesizedExpression(expression);
} else {
return expression;
}
}
/**
* Extract the string values from an `array` of string literals.
* @param array The array to unwrap.
*/
export function unwrapStringLiteralArray(array: t.Expression): string[] {
if (!isStringLiteralArray(array)) {
throw new BabelParseError(
array, 'Unexpected messageParts for `$localize` (expected an array of strings).');
}
return array.elements.map((str: t.StringLiteral) => str.value);
}
/**
* This expression is believed to be a call to a "lazy-load" template object helper function.
* This is expected to be of the form:
*
* ```ts
* function _templateObject() {
* var e = _taggedTemplateLiteral(['cooked string', 'raw string']);
* return _templateObject = function() { return e }, e
* }
* ```
*
* We unwrap this to return the call to `_taggedTemplateLiteral()`.
*
* @param call the call expression to unwrap
* @returns the call expression
*/
export function unwrapLazyLoadHelperCall(call: NodePath<t.CallExpression>):
NodePath<t.CallExpression> {
const callee = call.get('callee');
if (!callee.isIdentifier()) {
throw new BabelParseError(
callee.node,
'Unexpected lazy-load helper call (expected a call of the form `_templateObject()`).');
}
const lazyLoadBinding = call.scope.getBinding(callee.node.name);
if (!lazyLoadBinding) {
throw new BabelParseError(callee.node, 'Missing declaration for lazy-load helper function');
}
const lazyLoadFn = lazyLoadBinding.path;
if (!lazyLoadFn.isFunctionDeclaration()) {
throw new BabelParseError(
lazyLoadFn.node, 'Unexpected expression (expected a function declaration');
}
const returnedNode = getReturnedExpression(lazyLoadFn);
if (returnedNode.isCallExpression()) {
return returnedNode;
}
if (returnedNode.isIdentifier()) {
const identifierName = returnedNode.node.name;
const declaration = returnedNode.scope.getBinding(identifierName);
if (declaration === undefined) {
throw new BabelParseError(
returnedNode.node, 'Missing declaration for return value from helper.');
}
if (!declaration.path.isVariableDeclarator()) {
throw new BabelParseError(
declaration.path.node,
'Unexpected helper return value declaration (expected a variable declaration).');
}
const initializer = declaration.path.get('init');
if (!initializer.isCallExpression()) {
throw new BabelParseError(
declaration.path.node,
'Unexpected return value from helper (expected a call expression).');
}
// Remove the lazy load helper if this is the only reference to it.
if (lazyLoadBinding.references === 1) {
lazyLoadFn.remove();
}
return initializer;
}
return call;
}
function getReturnedExpression(fn: NodePath<t.FunctionDeclaration>): NodePath<t.Expression> {
const bodyStatements = fn.get('body').get('body');
for (const statement of bodyStatements) {
if (statement.isReturnStatement()) {
const argument = statement.get('argument');
if (argument.isSequenceExpression()) {
const expressions = argument.get('expressions');
return Array.isArray(expressions) ? expressions[expressions.length - 1] : expressions;
} else if (argument.isExpression()) {
return argument;
} else {
throw new BabelParseError(
statement.node, 'Invalid return argument in helper function (expected an expression).');
}
}
}
throw new BabelParseError(fn.node, 'Missing return statement in helper function.');
}
/**
* Is the given `node` an array of literal strings?
*
* @param node The node to test.
*/
export function isStringLiteralArray(node: t.Node): node is t.Expression&
{elements: t.StringLiteral[]} {
return t.isArrayExpression(node) && node.elements.every(element => t.isStringLiteral(element));
}
/**
* Are all the given `nodes` expressions?
* @param nodes The nodes to test.
*/
export function isArrayOfExpressions(nodes: t.Node[]): nodes is t.Expression[] {
return nodes.every(element => t.isExpression(element));
}
/** Options that affect how the `makeEsXXXTranslatePlugin()` functions work. */
export interface TranslatePluginOptions {
missingTranslation?: MissingTranslationStrategy;
localizeName?: string;
}
/**
* How to handle missing translations.
*/
export type MissingTranslationStrategy = 'error' | 'warning' | 'ignore';
/**
* Translate the text of the given message, using the given translations.
*
* Logs as warning if the translation is not available
*/
export function translate(
diagnostics: Diagnostics, translations: Record<string, ɵParsedTranslation>,
messageParts: TemplateStringsArray, substitutions: readonly any[],
missingTranslation: MissingTranslationStrategy): [TemplateStringsArray, readonly any[]] {
try {
return ɵtranslate(translations, messageParts, substitutions);
} catch (e) {
if (ɵisMissingTranslationError(e)) {
if (missingTranslation === 'error') {
diagnostics.error(e.message);
} else if (missingTranslation === 'warning') {
diagnostics.warn(e.message);
}
// Return the parsed message because this will have the meta blocks stripped
return [
ɵmakeTemplateObject(e.parsedMessage.messageParts, e.parsedMessage.messageParts),
substitutions
];
} else {
diagnostics.error(e.message);
return [messageParts, substitutions];
}
}
}
export class BabelParseError extends Error {
private readonly type = 'BabelParseError';
constructor(public node: t.Node, message: string) { super(message); }
}
export function isBabelParseError(e: any): e is BabelParseError {
return e.type === 'BabelParseError';
}