UNPKG

18 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt
7 * The complete set of authors may be found at
8 * http://polymer.github.io/AUTHORS.txt
9 * The complete set of contributors may be found at
10 * http://polymer.github.io/CONTRIBUTORS.txt
11 * Code distributed by Google as part of the polymer project is also
12 * subject to an additional IP rights grant found at
13 * http://polymer.github.io/PATENTS.txt
14 */
15Object.defineProperty(exports, "__esModule", { value: true });
16const babel = require("@babel/types");
17const dom5 = require("dom5/lib/index-next");
18const astValue = require("../javascript/ast-value");
19const javascript_parser_1 = require("../javascript/javascript-parser");
20const model_1 = require("../model/model");
21const p = dom5.predicates;
22const isTemplate = p.hasTagName('template');
23const isDataBindingTemplate = p.AND(isTemplate, p.OR(p.hasAttrValue('is', 'dom-bind'), p.hasAttrValue('is', 'dom-if'), p.hasAttrValue('is', 'dom-repeat'), p.parentMatches(p.OR(p.hasTagName('dom-bind'), p.hasTagName('dom-if'), p.hasTagName('dom-repeat'), p.hasTagName('dom-module')))));
24/**
25 * Given a node, return all databinding templates inside it.
26 *
27 * A template is "databinding" if polymer databinding expressions are expected
28 * to be evaluated inside. e.g. <template is='dom-if'> or <dom-module><template>
29 *
30 * Results include both direct and nested templates (e.g. dom-if inside
31 * dom-module).
32 */
33function getAllDataBindingTemplates(node) {
34 return dom5.queryAll(node, isDataBindingTemplate, dom5.childNodesIncludeTemplate);
35}
36exports.getAllDataBindingTemplates = getAllDataBindingTemplates;
37class DatabindingExpression {
38 constructor(sourceRange, expressionText, ast, limitation, document) {
39 this.warnings = [];
40 /**
41 * Toplevel properties on the model that are referenced in this expression.
42 *
43 * e.g. in {{foo(bar, baz.zod)}} the properties are foo, bar, and baz
44 * (but not zod).
45 */
46 this.properties = [];
47 this.sourceRange = sourceRange;
48 this.expressionText = expressionText;
49 this._expressionAst = ast;
50 this.locationOffset = {
51 line: sourceRange.start.line,
52 col: sourceRange.start.column
53 };
54 this._document = document;
55 this._extractPropertiesAndValidate(limitation);
56 }
57 /**
58 * Given an estree node in this databinding expression, give its source range.
59 */
60 sourceRangeForNode(node) {
61 if (!node || !node.loc) {
62 return;
63 }
64 const databindingRelativeSourceRange = {
65 file: this.sourceRange.file,
66 // Note: estree uses 1-indexed lines, but SourceRange uses 0 indexed.
67 start: { line: (node.loc.start.line - 1), column: node.loc.start.column },
68 end: { line: (node.loc.end.line - 1), column: node.loc.end.column }
69 };
70 return model_1.correctSourceRange(databindingRelativeSourceRange, this.locationOffset);
71 }
72 _extractPropertiesAndValidate(limitation) {
73 if (this._expressionAst.body.length !== 1) {
74 this.warnings.push(this._validationWarning(`Expected one expression, got ${this._expressionAst.body.length}`, this._expressionAst));
75 return;
76 }
77 const expressionStatement = this._expressionAst.body[0];
78 if (!babel.isExpressionStatement(expressionStatement)) {
79 this.warnings.push(this._validationWarning(`Expect an expression, not a ${expressionStatement.type}`, expressionStatement));
80 return;
81 }
82 let expression = expressionStatement.expression;
83 this._validateLimitation(expression, limitation);
84 if (babel.isUnaryExpression(expression) && expression.operator === '!') {
85 expression = expression.argument;
86 }
87 this._extractAndValidateSubExpression(expression, true);
88 }
89 _validateLimitation(expression, limitation) {
90 switch (limitation) {
91 case 'identifierOnly':
92 if (!babel.isIdentifier(expression)) {
93 this.warnings.push(this._validationWarning(`Expected just a name here, not an expression`, expression));
94 }
95 break;
96 case 'callExpression':
97 if (!babel.isCallExpression(expression)) {
98 this.warnings.push(this._validationWarning(`Expected a function call here.`, expression));
99 }
100 break;
101 case 'full':
102 break; // no checks needed
103 default:
104 const never = limitation;
105 throw new Error(`Got unknown limitation: ${never}`);
106 }
107 }
108 _extractAndValidateSubExpression(expression, callAllowed) {
109 if (babel.isUnaryExpression(expression) && expression.operator === '-') {
110 if (!babel.isNumericLiteral(expression.argument)) {
111 this.warnings.push(this._validationWarning('The - operator is only supported for writing negative numbers.', expression));
112 return;
113 }
114 this._extractAndValidateSubExpression(expression.argument, false);
115 return;
116 }
117 if (babel.isLiteral(expression)) {
118 return;
119 }
120 if (babel.isIdentifier(expression)) {
121 this.properties.push({
122 name: expression.name,
123 sourceRange: this.sourceRangeForNode(expression)
124 });
125 return;
126 }
127 if (babel.isMemberExpression(expression)) {
128 this._extractAndValidateSubExpression(expression.object, false);
129 return;
130 }
131 if (callAllowed && babel.isCallExpression(expression)) {
132 this._extractAndValidateSubExpression(expression.callee, false);
133 for (const arg of expression.arguments) {
134 this._extractAndValidateSubExpression(arg, false);
135 }
136 return;
137 }
138 this.warnings.push(this._validationWarning(`Only simple syntax is supported in Polymer databinding expressions. ` +
139 `${expression.type} not expected here.`, expression));
140 }
141 _validationWarning(message, node) {
142 return new model_1.Warning({
143 code: 'invalid-polymer-expression',
144 message,
145 sourceRange: this.sourceRangeForNode(node),
146 severity: model_1.Severity.WARNING,
147 parsedDocument: this._document,
148 });
149 }
150}
151exports.DatabindingExpression = DatabindingExpression;
152class AttributeDatabindingExpression extends DatabindingExpression {
153 constructor(astNode, isCompleteBinding, direction, eventName, attribute, sourceRange, expressionText, ast, document) {
154 super(sourceRange, expressionText, ast, 'full', document);
155 this.databindingInto = 'attribute';
156 this.astNode = astNode;
157 this.isCompleteBinding = isCompleteBinding;
158 this.direction = direction;
159 this.eventName = eventName;
160 this.attribute = attribute;
161 }
162}
163exports.AttributeDatabindingExpression = AttributeDatabindingExpression;
164class TextNodeDatabindingExpression extends DatabindingExpression {
165 constructor(direction, astNode, sourceRange, expressionText, ast, document) {
166 super(sourceRange, expressionText, ast, 'full', document);
167 this.databindingInto = 'text-node';
168 this.direction = direction;
169 this.astNode = astNode;
170 }
171}
172exports.TextNodeDatabindingExpression = TextNodeDatabindingExpression;
173class JavascriptDatabindingExpression extends DatabindingExpression {
174 constructor(astNode, sourceRange, expressionText, ast, kind, document) {
175 super(sourceRange, expressionText, ast, kind, document);
176 this.databindingInto = 'javascript';
177 this.astNode = astNode;
178 }
179}
180exports.JavascriptDatabindingExpression = JavascriptDatabindingExpression;
181/**
182 * Find and parse Polymer databinding expressions in HTML.
183 */
184function scanDocumentForExpressions(document) {
185 return extractDataBindingsFromTemplates(document, getAllDataBindingTemplates(document.ast));
186}
187exports.scanDocumentForExpressions = scanDocumentForExpressions;
188function scanDatabindingTemplateForExpressions(document, template) {
189 return extractDataBindingsFromTemplates(document, [template].concat([...getAllDataBindingTemplates(template.content)]));
190}
191exports.scanDatabindingTemplateForExpressions = scanDatabindingTemplateForExpressions;
192function extractDataBindingsFromTemplates(document, templates) {
193 const results = [];
194 const warnings = [];
195 for (const template of templates) {
196 for (const node of dom5.depthFirst(template.content)) {
197 if (dom5.isTextNode(node) && node.value) {
198 extractDataBindingsFromTextNode(document, node, results, warnings);
199 }
200 if (node.attrs) {
201 for (const attr of node.attrs) {
202 extractDataBindingsFromAttr(document, node, attr, results, warnings);
203 }
204 }
205 }
206 }
207 return { expressions: results, warnings };
208}
209function extractDataBindingsFromTextNode(document, node, results, warnings) {
210 const text = node.value || '';
211 const dataBindings = findDatabindingInString(text);
212 if (dataBindings.length === 0) {
213 return;
214 }
215 const nodeSourceRange = document.sourceRangeForNode(node);
216 if (!nodeSourceRange) {
217 return;
218 }
219 const startOfTextNodeOffset = document.sourcePositionToOffset(nodeSourceRange.start);
220 for (const dataBinding of dataBindings) {
221 const sourceRange = document.offsetsToSourceRange(dataBinding.startIndex + startOfTextNodeOffset, dataBinding.endIndex + startOfTextNodeOffset);
222 const parseResult = parseExpression(dataBinding.expressionText, sourceRange);
223 if (!parseResult) {
224 continue;
225 }
226 if (parseResult.type === 'failure') {
227 warnings.push(new model_1.Warning(Object.assign({ parsedDocument: document }, parseResult.warningish)));
228 }
229 else {
230 const expression = new TextNodeDatabindingExpression(dataBinding.direction, node, sourceRange, dataBinding.expressionText, parseResult.parsedFile.program, document);
231 for (const warning of expression.warnings) {
232 warnings.push(warning);
233 }
234 results.push(expression);
235 }
236 }
237}
238function extractDataBindingsFromAttr(document, node, attr, results, warnings) {
239 if (!attr.value) {
240 return;
241 }
242 const dataBindings = findDatabindingInString(attr.value);
243 const attributeValueRange = document.sourceRangeForAttributeValue(node, attr.name, true);
244 if (!attributeValueRange) {
245 return;
246 }
247 const attributeOffset = document.sourcePositionToOffset(attributeValueRange.start);
248 for (const dataBinding of dataBindings) {
249 const isFullAttributeBinding = dataBinding.startIndex === 2 &&
250 dataBinding.endIndex + 2 === attr.value.length;
251 let expressionText = dataBinding.expressionText;
252 let eventName = undefined;
253 if (dataBinding.direction === '{') {
254 const match = expressionText.match(/(.*)::(.*)/);
255 if (match) {
256 expressionText = match[1];
257 eventName = match[2];
258 }
259 }
260 const sourceRange = document.offsetsToSourceRange(dataBinding.startIndex + attributeOffset, dataBinding.endIndex + attributeOffset);
261 const parseResult = parseExpression(expressionText, sourceRange);
262 if (!parseResult) {
263 continue;
264 }
265 if (parseResult.type === 'failure') {
266 warnings.push(new model_1.Warning(Object.assign({ parsedDocument: document }, parseResult.warningish)));
267 }
268 else {
269 const expression = new AttributeDatabindingExpression(node, isFullAttributeBinding, dataBinding.direction, eventName, attr, sourceRange, expressionText, parseResult.parsedFile.program, document);
270 for (const warning of expression.warnings) {
271 warnings.push(warning);
272 }
273 results.push(expression);
274 }
275 }
276}
277function findDatabindingInString(str) {
278 const expressions = [];
279 const openers = /{{|\[\[/g;
280 let match;
281 while (match = openers.exec(str)) {
282 const matchedOpeners = match[0];
283 const startIndex = match.index + 2;
284 const direction = matchedOpeners === '{{' ? '{' : '[';
285 const closers = matchedOpeners === '{{' ? '}}' : ']]';
286 const endIndex = str.indexOf(closers, startIndex);
287 if (endIndex === -1) {
288 // No closers, this wasn't an expression after all.
289 break;
290 }
291 const expressionText = str.slice(startIndex, endIndex);
292 expressions.push({ startIndex, endIndex, expressionText, direction });
293 // Start looking for the next expression after the end of this one.
294 openers.lastIndex = endIndex + 2;
295 }
296 return expressions;
297}
298function transformPath(expression) {
299 return expression
300 // replace .0, .123, .kebab-case with ['0'], ['123'], ['kebab-case']
301 .replace(/\.([a-zA-Z_$]([\w:$*]*-+[\w:$*]*)+|[1-9][0-9]*|0)/g, '[\'$1\']')
302 // remove .* and .splices from the end of the paths
303 .replace(/\.(\*|splices)$/, '');
304}
305/**
306 * Transform polymer expression based on
307 * https://github.com/Polymer/polymer/blob/10aded461b1a107ed1cfc4a1d630149ad8508bda/lib/mixins/property-effects.html#L864
308 */
309function transformPolymerExprToJS(expression) {
310 const method = expression.match(/([^\s]+?)\(([\s\S]*)\)/);
311 if (method) {
312 const methodName = method[1];
313 if (method[2].trim()) {
314 // replace escaped commas with comma entity, split on un-escaped commas
315 const args = method[2].replace(/\\,/g, '&comma;').split(',');
316 return methodName + '(' + args.map(transformArg).join(',') + ')';
317 }
318 else {
319 return expression;
320 }
321 }
322 return transformPath(expression);
323}
324function transformArg(rawArg) {
325 const arg = rawArg
326 // replace comma entity with comma
327 .replace(/&comma;/g, ',')
328 // repair extra escape sequences; note only commas strictly
329 // need escaping, but we allow any other char to be escaped
330 // since its likely users will do this
331 .replace(/\\(.)/g, '\$1');
332 // detect literal value (must be String or Number)
333 const i = arg.search(/[^\s]/);
334 let fc = arg[i];
335 if (fc === '-') {
336 fc = arg[i + 1];
337 }
338 if (fc >= '0' && fc <= '9') {
339 fc = '#';
340 }
341 switch (fc) {
342 case '\'':
343 case '"':
344 return arg;
345 case '#':
346 return arg;
347 }
348 if (arg.indexOf('.') !== -1) {
349 return transformPath(arg);
350 }
351 return arg;
352}
353function parseExpression(content, expressionSourceRange) {
354 const expressionOffset = {
355 line: expressionSourceRange.start.line,
356 col: expressionSourceRange.start.column
357 };
358 const parseResult = javascript_parser_1.parseJs(transformPolymerExprToJS(content), expressionSourceRange.file, expressionOffset, 'polymer-expression-parse-error');
359 if (parseResult.type === 'success') {
360 return parseResult;
361 }
362 // The polymer databinding expression language allows for foo.0 and foo.*
363 // formats when accessing sub properties. These aren't valid JS, but we don't
364 // want to warn for them either. So just return undefined for now.
365 if (/\.(\*|\d+)/.test(content)) {
366 return undefined;
367 }
368 return parseResult;
369}
370function parseExpressionInJsStringLiteral(document, stringLiteral, kind) {
371 const warnings = [];
372 const result = {
373 databinding: undefined,
374 warnings
375 };
376 const sourceRangeForLiteral = document.sourceRangeForNode(stringLiteral);
377 const expressionText = astValue.expressionToValue(stringLiteral);
378 if (expressionText === undefined) {
379 // Should we warn here? It's potentially valid, just unanalyzable. Maybe
380 // just an info that someone could escalate to a warning/error?
381 warnings.push(new model_1.Warning({
382 code: 'unanalyzable-polymer-expression',
383 message: `Can only analyze databinding expressions in string literals.`,
384 severity: model_1.Severity.INFO,
385 sourceRange: sourceRangeForLiteral,
386 parsedDocument: document
387 }));
388 return result;
389 }
390 if (typeof expressionText !== 'string') {
391 warnings.push(new model_1.Warning({
392 code: 'invalid-polymer-expression',
393 message: `Expected a string, got a ${typeof expressionText}.`,
394 sourceRange: sourceRangeForLiteral,
395 severity: model_1.Severity.WARNING,
396 parsedDocument: document
397 }));
398 return result;
399 }
400 const sourceRange = {
401 file: sourceRangeForLiteral.file,
402 start: {
403 column: sourceRangeForLiteral.start.column + 1,
404 line: sourceRangeForLiteral.start.line
405 },
406 end: {
407 column: sourceRangeForLiteral.end.column - 1,
408 line: sourceRangeForLiteral.end.line
409 }
410 };
411 const parsed = parseExpression(expressionText, sourceRange);
412 if (parsed && parsed.type === 'failure') {
413 warnings.push(new model_1.Warning(Object.assign({ parsedDocument: document }, parsed.warningish)));
414 }
415 else if (parsed && parsed.type === 'success') {
416 result.databinding = new JavascriptDatabindingExpression(stringLiteral, sourceRange, expressionText, parsed.parsedFile.program, kind, document);
417 for (const warning of result.databinding.warnings) {
418 warnings.push(warning);
419 }
420 }
421 return result;
422}
423exports.parseExpressionInJsStringLiteral = parseExpressionInJsStringLiteral;
424//# sourceMappingURL=expression-scanner.js.map
\No newline at end of file