UNPKG

36 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2015 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 */
15var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
16 return new (P || (P = Promise))(function (resolve, reject) {
17 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
18 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
19 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
20 step((generator = generator.apply(thisArg, _arguments || [])).next());
21 });
22};
23Object.defineProperty(exports, "__esModule", { value: true });
24const generator_1 = require("@babel/generator");
25const babel = require("@babel/types");
26const doctrine = require("doctrine");
27const model_1 = require("../model/model");
28const declaration_property_handlers_1 = require("../polymer/declaration-property-handlers");
29const polymer_element_1 = require("../polymer/polymer-element");
30const polymer2_config_1 = require("../polymer/polymer2-config");
31const polymer2_mixin_scanner_1 = require("../polymer/polymer2-mixin-scanner");
32const astValue = require("./ast-value");
33const ast_value_1 = require("./ast-value");
34const esutil = require("./esutil");
35const esutil_1 = require("./esutil");
36const jsdoc = require("./jsdoc");
37/**
38 * Find and classify classes from source code.
39 *
40 * Currently this has a bunch of Polymer stuff baked in that shouldn't be here
41 * in order to support generating only one feature for stuff that's essentially
42 * more specific kinds of classes, like Elements, PolymerElements, Mixins, etc.
43 *
44 * In a future change we'll add a mechanism whereby plugins can claim and
45 * specialize classes.
46 */
47class ClassScanner {
48 scan(document, visit) {
49 return __awaiter(this, void 0, void 0, function* () {
50 const classFinder = new ClassFinder(document);
51 const elementDefinitionFinder = new CustomElementsDefineCallFinder(document);
52 const prototypeMemberFinder = new PrototypeMemberFinder(document);
53 yield visit(prototypeMemberFinder);
54 const mixinFinder = new polymer2_mixin_scanner_1.MixinVisitor(document, prototypeMemberFinder);
55 // Find all classes and all calls to customElements.define()
56 yield Promise.all([
57 visit(classFinder),
58 visit(elementDefinitionFinder),
59 visit(mixinFinder),
60 ]);
61 const mixins = mixinFinder.mixins;
62 const elementDefinitionsByClassName = new Map();
63 // For classes that show up as expressions in the second argument position
64 // of a customElements.define call.
65 const elementDefinitionsByClassExpression = new Map();
66 for (const defineCall of elementDefinitionFinder.calls) {
67 // MaybeChainedIdentifier is invented below. It's like Identifier, but it
68 // includes 'Polymer.Element' as a name.
69 if (defineCall.class_.type === 'MaybeChainedIdentifier') {
70 elementDefinitionsByClassName.set(defineCall.class_.name, defineCall);
71 }
72 else {
73 elementDefinitionsByClassExpression.set(defineCall.class_, defineCall);
74 }
75 }
76 // TODO(rictic): emit ElementDefineCallFeatures for define calls that don't
77 // map to any local classes?
78 const mixinClassExpressions = new Set();
79 for (const mixin of mixins) {
80 if (mixin.classAstNode) {
81 mixinClassExpressions.add(mixin.classAstNode);
82 }
83 }
84 // Next we want to distinguish custom elements from other classes.
85 const customElements = [];
86 const normalClasses = [];
87 const classMap = new Map();
88 for (const class_ of classFinder.classes) {
89 if (class_.astNode.language === 'js' &&
90 mixinClassExpressions.has(class_.astNode.node)) {
91 // This class is a mixin and has already been handled as such.
92 continue;
93 }
94 if (class_.name) {
95 classMap.set(class_.name, class_);
96 }
97 // Class expressions inside the customElements.define call
98 if (babel.isClassExpression(class_.astNode.node)) {
99 const definition = elementDefinitionsByClassExpression.get(class_.astNode.node);
100 if (definition) {
101 customElements.push({ class_, definition });
102 continue;
103 }
104 }
105 // Classes whose names are referenced in a same-file customElements.define
106 const definition = elementDefinitionsByClassName.get(class_.name) ||
107 elementDefinitionsByClassName.get(class_.localName);
108 if (definition) {
109 customElements.push({ class_, definition });
110 continue;
111 }
112 // Classes explicitly defined as elements in their jsdoc tags.
113 // TODO(justinfagnani): remove @polymerElement support
114 if (jsdoc.hasTag(class_.jsdoc, 'customElement') ||
115 jsdoc.hasTag(class_.jsdoc, 'polymerElement')) {
116 customElements.push({ class_ });
117 continue;
118 }
119 // Classes that aren't custom elements, or at least, aren't obviously.
120 normalClasses.push(class_);
121 }
122 for (const [name, members] of prototypeMemberFinder.members) {
123 if (classMap.has(name)) {
124 const cls = classMap.get(name);
125 cls.finishInitialization(members.methods, members.properties);
126 }
127 }
128 const scannedFeatures = [];
129 for (const element of customElements) {
130 scannedFeatures.push(this._makeElementFeature(element, document));
131 }
132 for (const scannedClass of normalClasses) {
133 scannedFeatures.push(scannedClass);
134 }
135 for (const mixin of mixins) {
136 scannedFeatures.push(mixin);
137 }
138 const collapsedClasses = this.collapseEphemeralSuperclasses(scannedFeatures, classFinder);
139 return {
140 features: collapsedClasses,
141 warnings: [
142 ...elementDefinitionFinder.warnings,
143 ...classFinder.warnings,
144 ...mixinFinder.warnings,
145 ]
146 };
147 });
148 }
149 /**
150 * Handle the pattern where a class's superclass is declared as a separate
151 * variable, usually so that mixins can be applied in a way that is compatible
152 * with the Closure compiler. We consider a class ephemeral if:
153 *
154 * 1) It is the superclass of one or more classes.
155 * 2) It is declared using a const, let, or var.
156 * 3) It is annotated as @private.
157 */
158 collapseEphemeralSuperclasses(allClasses, classFinder) {
159 const possibleEphemeralsById = new Map();
160 const classesBySuperClassId = new Map();
161 for (const cls of allClasses) {
162 if (cls.name === undefined) {
163 continue;
164 }
165 if (classFinder.fromVariableDeclarators.has(cls) &&
166 cls.privacy === 'private') {
167 possibleEphemeralsById.set(cls.name, cls);
168 }
169 if (cls.superClass !== undefined) {
170 const superClassId = cls.superClass.identifier;
171 const childClasses = classesBySuperClassId.get(superClassId);
172 if (childClasses === undefined) {
173 classesBySuperClassId.set(superClassId, [cls]);
174 }
175 else {
176 childClasses.push(cls);
177 }
178 }
179 }
180 const ephemerals = new Set();
181 for (const [superClassId, childClasses] of classesBySuperClassId) {
182 const superClass = possibleEphemeralsById.get(superClassId);
183 if (superClass === undefined) {
184 continue;
185 }
186 let isEphemeral = false;
187 for (const childClass of childClasses) {
188 // Feature properties are readonly, hence this hacky cast. We could also
189 // make a new feature, but then we'd need a good way to clone a feature.
190 // It's pretty safe because we're still in the construction phase for
191 // scanned classes, so we know nothing else could be relying on the
192 // previous value yet.
193 childClass.superClass = superClass.superClass;
194 childClass.mixins.push(...superClass.mixins);
195 isEphemeral = true;
196 }
197 if (isEphemeral) {
198 ephemerals.add(superClass);
199 }
200 }
201 return allClasses.filter((cls) => !ephemerals.has(cls));
202 }
203 _makeElementFeature(element, document) {
204 const class_ = element.class_;
205 const astNode = element.class_.astNode;
206 const docs = element.class_.jsdoc;
207 const customElementTag = jsdoc.getTag(class_.jsdoc, 'customElement');
208 let tagName = undefined;
209 if (element.definition &&
210 element.definition.tagName.type === 'string-literal') {
211 tagName = element.definition.tagName.value;
212 }
213 else if (customElementTag && customElementTag.description) {
214 tagName = customElementTag.description;
215 }
216 else if (babel.isClassExpression(astNode.node) ||
217 babel.isClassDeclaration(astNode.node)) {
218 tagName = polymer2_config_1.getIsValue(astNode.node);
219 }
220 let warnings = [];
221 let scannedElement;
222 let methods = new Map();
223 let staticMethods = new Map();
224 let observers = [];
225 // This will cover almost all classes, except those defined only by
226 // applying a mixin. e.g. const MyElem = Mixin(HTMLElement)
227 if (babel.isClassExpression(astNode.node) ||
228 babel.isClassDeclaration(astNode.node)) {
229 const observersResult = this._getObservers(astNode.node, document);
230 observers = [];
231 if (observersResult) {
232 observers = observersResult.observers;
233 warnings = warnings.concat(observersResult.warnings);
234 }
235 const polymerProps = polymer2_config_1.getPolymerProperties(astNode.node, document);
236 for (const prop of polymerProps) {
237 const constructorProp = class_.properties.get(prop.name);
238 let finalProp;
239 if (constructorProp) {
240 finalProp = polymer_element_1.mergePropertyDeclarations(constructorProp, prop);
241 }
242 else {
243 finalProp = prop;
244 }
245 class_.properties.set(prop.name, finalProp);
246 }
247 methods = esutil_1.getMethods(astNode.node, document);
248 staticMethods = esutil_1.getStaticMethods(astNode.node, document);
249 }
250 const extends_ = getExtendsTypeName(docs);
251 // TODO(justinfagnani): Infer mixin applications and superclass from AST.
252 scannedElement = new polymer_element_1.ScannedPolymerElement({
253 className: class_.name,
254 tagName,
255 astNode,
256 statementAst: class_.statementAst,
257 properties: [...class_.properties.values()],
258 methods,
259 staticMethods,
260 observers,
261 events: astNode.language === 'js' ?
262 esutil.getEventComments(astNode.node) :
263 new Map(),
264 attributes: new Map(),
265 behaviors: [],
266 extends: extends_,
267 listeners: [],
268 description: class_.description,
269 sourceRange: class_.sourceRange,
270 superClass: class_.superClass,
271 jsdoc: class_.jsdoc,
272 abstract: class_.abstract,
273 mixins: class_.mixins,
274 privacy: class_.privacy,
275 isLegacyFactoryCall: false,
276 });
277 if (babel.isClassExpression(astNode.node) ||
278 babel.isClassDeclaration(astNode.node)) {
279 const observedAttributes = this._getObservedAttributes(astNode.node, document);
280 if (observedAttributes != null) {
281 // If a class defines observedAttributes, it overrides what the base
282 // classes defined.
283 // TODO(justinfagnani): define and handle composition patterns.
284 scannedElement.attributes.clear();
285 for (const attr of observedAttributes) {
286 scannedElement.attributes.set(attr.name, attr);
287 }
288 }
289 }
290 warnings.forEach((w) => scannedElement.warnings.push(w));
291 return scannedElement;
292 }
293 _getObservers(node, document) {
294 const returnedValue = polymer2_config_1.getStaticGetterValue(node, 'observers');
295 if (returnedValue) {
296 return declaration_property_handlers_1.extractObservers(returnedValue, document);
297 }
298 }
299 _getObservedAttributes(node, document) {
300 const returnedValue = polymer2_config_1.getStaticGetterValue(node, 'observedAttributes');
301 if (returnedValue && babel.isArrayExpression(returnedValue)) {
302 return this._extractAttributesFromObservedAttributes(returnedValue, document);
303 }
304 }
305 /**
306 * Extract attributes from the array expression inside a static
307 * observedAttributes method.
308 *
309 * e.g.
310 * static get observedAttributes() {
311 * return [
312 * /** @type {boolean} When given the element is totally inactive *\/
313 * 'disabled',
314 * /** @type {boolean} When given the element is expanded *\/
315 * 'open'
316 * ];
317 * }
318 */
319 _extractAttributesFromObservedAttributes(arry, document) {
320 const results = [];
321 for (const expr of arry.elements) {
322 const value = astValue.expressionToValue(expr);
323 if (value && typeof value === 'string') {
324 let description = '';
325 let type = null;
326 const comment = esutil.getAttachedComment(expr);
327 if (comment) {
328 const annotation = jsdoc.parseJsdoc(comment);
329 description = annotation.description || description;
330 const tags = annotation.tags || [];
331 for (const tag of tags) {
332 if (tag.title === 'type') {
333 type = type || doctrine.type.stringify(tag.type);
334 }
335 // TODO(justinfagnani): this appears wrong, any tag could have a
336 // description do we really let any tag's description override
337 // the previous?
338 description = description || tag.description || '';
339 }
340 }
341 const attribute = {
342 name: value,
343 description: description,
344 sourceRange: document.sourceRangeForNode(expr),
345 astNode: { language: 'js', containingDocument: document, node: expr },
346 warnings: [],
347 };
348 if (type) {
349 attribute.type = type;
350 }
351 results.push(attribute);
352 }
353 }
354 return results;
355 }
356}
357exports.ClassScanner = ClassScanner;
358class PrototypeMemberFinder {
359 constructor(document) {
360 this.members = new model_1.MapWithDefault(() => ({
361 methods: new Map(),
362 properties: new Map()
363 }));
364 this._document = document;
365 }
366 enterExpressionStatement(node) {
367 if (babel.isAssignmentExpression(node.expression)) {
368 this._createMemberFromAssignment(node.expression, getJSDocAnnotationForNode(node));
369 }
370 else if (babel.isMemberExpression(node.expression)) {
371 this._createMemberFromMemberExpression(node.expression, getJSDocAnnotationForNode(node));
372 }
373 }
374 _createMemberFromAssignment(node, jsdocAnn) {
375 if (!babel.isMemberExpression(node.left) ||
376 !babel.isMemberExpression(node.left.object) ||
377 !babel.isIdentifier(node.left.property)) {
378 return;
379 }
380 const leftExpr = node.left.object;
381 const leftProperty = node.left.property;
382 const cls = ast_value_1.getIdentifierName(leftExpr.object);
383 if (!cls || ast_value_1.getIdentifierName(leftExpr.property) !== 'prototype') {
384 return;
385 }
386 if (babel.isFunctionExpression(node.right)) {
387 const prop = this._createMethodFromExpression(leftProperty.name, node.right, jsdocAnn);
388 if (prop) {
389 this._addMethodToClass(cls, prop);
390 }
391 }
392 else {
393 const method = this._createPropertyFromExpression(leftProperty.name, node, jsdocAnn);
394 if (method) {
395 this._addPropertyToClass(cls, method);
396 }
397 }
398 }
399 _addMethodToClass(cls, member) {
400 const classMembers = this.members.get(cls);
401 classMembers.methods.set(member.name, member);
402 }
403 _addPropertyToClass(cls, member) {
404 const classMembers = this.members.get(cls);
405 classMembers.properties.set(member.name, member);
406 }
407 _createMemberFromMemberExpression(node, jsdocAnn) {
408 const left = node.object;
409 // we only want `something.prototype.member`
410 if (!babel.isIdentifier(node.property) || !babel.isMemberExpression(left) ||
411 ast_value_1.getIdentifierName(left.property) !== 'prototype') {
412 return;
413 }
414 const cls = ast_value_1.getIdentifierName(left.object);
415 if (!cls) {
416 return;
417 }
418 if (jsdoc.hasTag(jsdocAnn, 'function')) {
419 const method = this._createMethodFromExpression(node.property.name, node, jsdocAnn);
420 if (method) {
421 this._addMethodToClass(cls, method);
422 }
423 }
424 else {
425 const prop = this._createPropertyFromExpression(node.property.name, node, jsdocAnn);
426 if (prop) {
427 this._addPropertyToClass(cls, prop);
428 }
429 }
430 }
431 _createPropertyFromExpression(name, node, jsdocAnn) {
432 let description;
433 let type;
434 let readOnly = false;
435 const privacy = esutil_1.getOrInferPrivacy(name, jsdocAnn);
436 const sourceRange = this._document.sourceRangeForNode(node);
437 const warnings = [];
438 if (jsdocAnn) {
439 description = jsdoc.getDescription(jsdocAnn);
440 readOnly = jsdoc.hasTag(jsdocAnn, 'readonly');
441 }
442 let detectedType;
443 if (babel.isAssignmentExpression(node)) {
444 detectedType =
445 esutil_1.getClosureType(node.right, jsdocAnn, sourceRange, this._document);
446 }
447 else {
448 detectedType =
449 esutil_1.getClosureType(node, jsdocAnn, sourceRange, this._document);
450 }
451 if (detectedType.successful) {
452 type = detectedType.value;
453 }
454 else {
455 warnings.push(detectedType.error);
456 type = '?';
457 }
458 return {
459 name,
460 astNode: { language: 'js', containingDocument: this._document, node },
461 type,
462 jsdoc: jsdocAnn,
463 sourceRange,
464 description,
465 privacy,
466 warnings,
467 readOnly,
468 };
469 }
470 _createMethodFromExpression(name, node, jsdocAnn) {
471 let description;
472 let ret;
473 const privacy = esutil_1.getOrInferPrivacy(name, jsdocAnn);
474 const params = new Map();
475 if (jsdocAnn) {
476 description = jsdoc.getDescription(jsdocAnn);
477 ret = esutil_1.getReturnFromAnnotation(jsdocAnn);
478 if (babel.isFunctionExpression(node)) {
479 (node.params || []).forEach((nodeParam) => {
480 const param = esutil_1.toMethodParam(nodeParam, jsdocAnn);
481 params.set(param.name, param);
482 });
483 }
484 else {
485 for (const tag of (jsdocAnn.tags || [])) {
486 if (tag.title !== 'param' || !tag.name) {
487 continue;
488 }
489 let tagType;
490 let tagDescription;
491 if (tag.type) {
492 tagType = doctrine.type.stringify(tag.type);
493 }
494 if (tag.description) {
495 tagDescription = tag.description;
496 }
497 params.set(tag.name, { name: tag.name, type: tagType, description: tagDescription });
498 }
499 }
500 }
501 if (ret === undefined && babel.isFunctionExpression(node)) {
502 ret = esutil_1.inferReturnFromBody(node);
503 }
504 return {
505 name,
506 type: ret !== undefined ? ret.type : undefined,
507 description,
508 sourceRange: this._document.sourceRangeForNode(node),
509 warnings: [],
510 astNode: { language: 'js', containingDocument: this._document, node },
511 jsdoc: jsdocAnn,
512 params: Array.from(params.values()),
513 return: ret,
514 privacy
515 };
516 }
517}
518exports.PrototypeMemberFinder = PrototypeMemberFinder;
519/**
520 * Finds all classes and matches them up with their best jsdoc comment.
521 */
522class ClassFinder {
523 constructor(document) {
524 this.classes = [];
525 this.warnings = [];
526 this.fromVariableDeclarators = new Set();
527 this.alreadyMatched = new Set();
528 this._document = document;
529 }
530 enterAssignmentExpression(node, _parent, path) {
531 this.handleGeneralAssignment(astValue.getIdentifierName(node.left), node.right, path);
532 }
533 enterVariableDeclarator(node, _parent, path) {
534 if (node.init) {
535 this.handleGeneralAssignment(astValue.getIdentifierName(node.id), node.init, path);
536 }
537 }
538 enterFunctionDeclaration(node, _parent, path) {
539 this.handleGeneralAssignment(astValue.getIdentifierName(node.id), node.body, path);
540 }
541 /** Generalizes over variable declarators and assignment expressions. */
542 handleGeneralAssignment(assignedName, value, path) {
543 const doc = jsdoc.parseJsdoc(esutil.getBestComment(path) || '');
544 if (babel.isClassExpression(value)) {
545 const name = assignedName ||
546 value.id && astValue.getIdentifierName(value.id) || undefined;
547 this._classFound(name, doc, value, path);
548 }
549 else if (jsdoc.hasTag(doc, 'constructor') ||
550 // TODO(justinfagnani): remove @polymerElement support
551 jsdoc.hasTag(doc, 'customElement') ||
552 jsdoc.hasTag(doc, 'polymerElement')) {
553 this._classFound(assignedName, doc, value, path);
554 }
555 }
556 enterClassExpression(node, parent, path) {
557 // Class expressions may be on the right hand side of assignments, so
558 // we may have already handled this expression from the parent or
559 // grandparent node. Class declarations can't be on the right hand side of
560 // assignments, so they'll definitely only be handled once.
561 if (this.alreadyMatched.has(node)) {
562 return;
563 }
564 const name = node.id ? astValue.getIdentifierName(node.id) : undefined;
565 const comment = esutil.getAttachedComment(node) ||
566 esutil.getAttachedComment(parent) || '';
567 this._classFound(name, jsdoc.parseJsdoc(comment), node, path);
568 }
569 enterClassDeclaration(node, parent, path) {
570 const name = astValue.getIdentifierName(node.id);
571 const comment = esutil.getAttachedComment(node) ||
572 esutil.getAttachedComment(parent) || '';
573 this._classFound(name, jsdoc.parseJsdoc(comment), node, path);
574 }
575 _classFound(name, doc, astNode, path) {
576 const namespacedName = name && ast_value_1.getNamespacedIdentifier(name, doc);
577 const warnings = [];
578 const properties = extractPropertiesFromClass(astNode, this._document);
579 const methods = esutil_1.getMethods(astNode, this._document);
580 const constructorMethod = esutil_1.getConstructorMethod(astNode, this._document);
581 const scannedClass = new model_1.ScannedClass(namespacedName, name, { language: 'js', containingDocument: this._document, node: astNode }, esutil.getCanonicalStatement(path), doc, (doc.description || '').trim(), this._document.sourceRangeForNode(astNode), properties, methods, constructorMethod, esutil_1.getStaticMethods(astNode, this._document), this._getExtends(astNode, doc, warnings, this._document, path), jsdoc.getMixinApplications(this._document, astNode, doc, warnings, path), esutil_1.getOrInferPrivacy(namespacedName || '', doc), warnings, jsdoc.hasTag(doc, 'abstract'), jsdoc.extractDemos(doc));
582 this.classes.push(scannedClass);
583 if (babel.isVariableDeclarator(path.node)) {
584 this.fromVariableDeclarators.add(scannedClass);
585 }
586 if (babel.isClassExpression(astNode)) {
587 this.alreadyMatched.add(astNode);
588 }
589 }
590 /**
591 * Returns the name of the superclass, if any.
592 */
593 _getExtends(node, docs, warnings, document, path) {
594 const extendsId = getExtendsTypeName(docs);
595 // prefer @extends annotations over extends clauses
596 if (extendsId !== undefined) {
597 // TODO(justinfagnani): we need source ranges for jsdoc annotations
598 const sourceRange = document.sourceRangeForNode(node);
599 if (extendsId == null) {
600 warnings.push(new model_1.Warning({
601 code: 'class-extends-annotation-no-id',
602 message: '@extends annotation with no identifier',
603 severity: model_1.Severity.WARNING,
604 sourceRange,
605 parsedDocument: this._document
606 }));
607 }
608 else {
609 return new model_1.ScannedReference('class', extendsId, sourceRange, undefined, path);
610 }
611 }
612 else if (babel.isClassDeclaration(node) || babel.isClassExpression(node)) {
613 // If no @extends tag, look for a superclass.
614 const superClass = node.superClass;
615 if (superClass != null) {
616 let extendsId = ast_value_1.getIdentifierName(superClass);
617 if (extendsId != null) {
618 if (extendsId.startsWith('window.')) {
619 extendsId = extendsId.substring('window.'.length);
620 }
621 const sourceRange = document.sourceRangeForNode(superClass);
622 return new model_1.ScannedReference('class', extendsId, sourceRange, {
623 language: 'js',
624 node: node.superClass,
625 containingDocument: document
626 }, path);
627 }
628 }
629 }
630 }
631}
632/** Finds calls to customElements.define() */
633class CustomElementsDefineCallFinder {
634 constructor(document) {
635 this.warnings = [];
636 this.calls = [];
637 this._document = document;
638 }
639 enterCallExpression(node) {
640 const callee = astValue.getIdentifierName(node.callee);
641 if (!(callee === 'window.customElements.define' ||
642 callee === 'customElements.define')) {
643 return;
644 }
645 const tagNameExpression = this._getTagNameExpression(node.arguments[0]);
646 if (tagNameExpression == null) {
647 return;
648 }
649 const elementClassExpression = this._getElementClassExpression(node.arguments[1]);
650 if (elementClassExpression == null) {
651 return;
652 }
653 this.calls.push({ tagName: tagNameExpression, class_: elementClassExpression });
654 }
655 _getTagNameExpression(expression) {
656 if (expression == null) {
657 return;
658 }
659 const tryForLiteralString = astValue.expressionToValue(expression);
660 if (tryForLiteralString != null &&
661 typeof tryForLiteralString === 'string') {
662 return {
663 type: 'string-literal',
664 value: tryForLiteralString,
665 sourceRange: this._document.sourceRangeForNode(expression)
666 };
667 }
668 if (babel.isMemberExpression(expression)) {
669 // Might be something like MyElement.is
670 const isPropertyNameIs = (babel.isIdentifier(expression.property) &&
671 expression.property.name === 'is') ||
672 (astValue.expressionToValue(expression.property) === 'is');
673 const className = astValue.getIdentifierName(expression.object);
674 if (isPropertyNameIs && className) {
675 return {
676 type: 'is',
677 className,
678 classNameSourceRange: this._document.sourceRangeForNode(expression.object)
679 };
680 }
681 }
682 this.warnings.push(new model_1.Warning({
683 code: 'cant-determine-element-tagname',
684 message: `Unable to evaluate this expression down to a definitive string ` +
685 `tagname.`,
686 severity: model_1.Severity.WARNING,
687 sourceRange: this._document.sourceRangeForNode(expression),
688 parsedDocument: this._document
689 }));
690 return undefined;
691 }
692 _getElementClassExpression(elementDefn) {
693 if (elementDefn == null) {
694 return null;
695 }
696 const className = astValue.getIdentifierName(elementDefn);
697 if (className) {
698 return {
699 type: 'MaybeChainedIdentifier',
700 name: className,
701 sourceRange: this._document.sourceRangeForNode(elementDefn)
702 };
703 }
704 if (babel.isClassExpression(elementDefn)) {
705 return elementDefn;
706 }
707 this.warnings.push(new model_1.Warning({
708 code: 'cant-determine-element-class',
709 message: `Unable to evaluate this expression down to a class reference.`,
710 severity: model_1.Severity.WARNING,
711 sourceRange: this._document.sourceRangeForNode(elementDefn),
712 parsedDocument: this._document,
713 }));
714 return null;
715 }
716}
717function extractPropertiesFromClass(astNode, document) {
718 const properties = new Map();
719 if (!babel.isClass(astNode)) {
720 return properties;
721 }
722 const construct = esutil_1.getConstructorClassMethod(astNode);
723 if (construct) {
724 const props = extractPropertiesFromConstructor(construct, document);
725 for (const prop of props.values()) {
726 properties.set(prop.name, prop);
727 }
728 }
729 for (const prop of esutil
730 .extractPropertiesFromClassOrObjectBody(astNode, document)
731 .values()) {
732 const existing = properties.get(prop.name);
733 if (!existing) {
734 properties.set(prop.name, prop);
735 }
736 else {
737 properties.set(prop.name, {
738 name: prop.name,
739 astNode: prop.astNode,
740 type: prop.type || existing.type,
741 jsdoc: prop.jsdoc,
742 sourceRange: prop.sourceRange,
743 description: prop.description || existing.description,
744 privacy: prop.privacy || existing.privacy,
745 warnings: prop.warnings,
746 readOnly: prop.readOnly === undefined ?
747 existing.readOnly : prop.readOnly
748 });
749 }
750 }
751 return properties;
752}
753exports.extractPropertiesFromClass = extractPropertiesFromClass;
754function extractPropertyFromExpressionStatement(statement, document) {
755 let name;
756 let astNode;
757 let defaultValue;
758 if (babel.isAssignmentExpression(statement.expression)) {
759 // statements like:
760 // /** @public The foo. */
761 // this.foo = baz;
762 name = getPropertyNameOnThisExpression(statement.expression.left);
763 astNode = statement.expression.left;
764 defaultValue = generator_1.default(statement.expression.right).code;
765 }
766 else if (babel.isMemberExpression(statement.expression)) {
767 // statements like:
768 // /** @public The foo. */
769 // this.foo;
770 name = getPropertyNameOnThisExpression(statement.expression);
771 astNode = statement;
772 }
773 else {
774 return null;
775 }
776 if (name === undefined) {
777 return null;
778 }
779 const annotation = getJSDocAnnotationForNode(statement);
780 if (!annotation) {
781 return null;
782 }
783 return {
784 name,
785 astNode: { language: 'js', containingDocument: document, node: astNode },
786 type: getTypeFromAnnotation(annotation),
787 default: defaultValue,
788 jsdoc: annotation,
789 sourceRange: document.sourceRangeForNode(astNode),
790 description: jsdoc.getDescription(annotation),
791 privacy: esutil_1.getOrInferPrivacy(name, annotation),
792 warnings: [],
793 readOnly: jsdoc.hasTag(annotation, 'const'),
794 };
795}
796function extractPropertiesFromConstructor(method, document) {
797 const properties = new Map();
798 for (const statement of method.body.body) {
799 if (!babel.isExpressionStatement(statement)) {
800 continue;
801 }
802 const prop = extractPropertyFromExpressionStatement(statement, document);
803 if (!prop) {
804 continue;
805 }
806 properties.set(prop.name, prop);
807 }
808 return properties;
809}
810function getJSDocAnnotationForNode(node) {
811 const comment = esutil.getAttachedComment(node);
812 const jsdocAnn = comment === undefined ? undefined : jsdoc.parseJsdoc(comment);
813 if (!jsdocAnn || jsdocAnn.tags.length === 0) {
814 // The comment only counts if there's a jsdoc annotation in there
815 // somewhere.
816 // Otherwise it's just an assignment, maybe to a property in a
817 // super class or something.
818 return undefined;
819 }
820 return jsdocAnn;
821}
822function getTypeFromAnnotation(jsdocAnn) {
823 const typeTag = jsdoc.getTag(jsdocAnn, 'type');
824 let type = undefined;
825 if (typeTag && typeTag.type) {
826 type = doctrine.type.stringify(typeTag.type);
827 }
828 return type;
829}
830function getPropertyNameOnThisExpression(node) {
831 if (!babel.isMemberExpression(node) || node.computed ||
832 !babel.isThisExpression(node.object) ||
833 !babel.isIdentifier(node.property)) {
834 return;
835 }
836 return node.property.name;
837}
838/**
839 * Return the type name from the first @extends annotation. Supports either
840 * `@extends {SuperClass}` or `@extends SuperClass` forms.
841 */
842function getExtendsTypeName(docs) {
843 const tag = jsdoc.getTag(docs, 'extends');
844 if (!tag) {
845 return undefined;
846 }
847 return tag.type ? doctrine.type.stringify(tag.type) : tag.name;
848}
849//# sourceMappingURL=class-scanner.js.map
\No newline at end of file