polymer-analyzer
Version:
Static analysis for Web Components
380 lines • 14.1 kB
JavaScript
"use strict";
/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
Object.defineProperty(exports, "__esModule", { value: true });
const analysis_1 = require("./analysis");
const immutable_1 = require("./immutable");
const map_1 = require("./map");
const resolvable_1 = require("./resolvable");
/**
* The metadata for all features and elements defined in one document
*/
class ScannedDocument {
constructor(document, features, warnings = []) {
this.isInline = false;
this.document = document;
this.features = features;
this.warnings = warnings;
this.isInline = document.isInline;
}
get sourceRange() {
return this.document.sourceRange;
}
get astNode() {
return this.document.astNode;
}
get url() {
return this.document.url;
}
/**
* Gets all features in this scanned document and all inline documents it
* contains.
*/
getNestedFeatures() {
const result = [];
this._getNestedFeatures(result);
return result;
}
_getNestedFeatures(features) {
for (const feature of this.features) {
// Ad hoc test needed here to avoid a problematic import loop.
const maybeInlineDoc = feature;
if (maybeInlineDoc.constructor.name === 'ScannedInlineDocument' &&
maybeInlineDoc.scannedDocument) {
maybeInlineDoc.scannedDocument._getNestedFeatures(features);
}
else {
features.push(feature);
}
}
}
}
exports.ScannedDocument = ScannedDocument;
function isDeclaredWithStatement(feature) {
return feature.statementAst !== undefined;
}
class Document {
constructor(base, analyzer, languageAnalysis) {
this.kinds = new Set(['document']);
this.identifiers = new Set();
this._localFeatures = new Set();
this._localFeaturesByStatement = new map_1.MapWithDefault(() => new Set());
/**
* To handle recursive dependency graphs we must track whether we've started
* resolving this Document so that we can reliably early exit even if one
* of our dependencies tries to resolve this document.
*/
this._begunResolving = false;
/**
* True after this document and all of its children are finished resolving.
*/
this._doneResolving = false;
this._featuresByKind = null;
this._featuresByKindAndId = null;
if (base == null) {
throw new Error('base is null');
}
if (analyzer == null) {
throw new Error('analyzer is null');
}
this._scannedDocument = base;
this._analysisContext = analyzer;
this.languageAnalysis = languageAnalysis;
if (!base.isInline) {
immutable_1.unsafeAsMutable(this.identifiers).add(this.url);
}
immutable_1.unsafeAsMutable(this.kinds).add(`${this.parsedDocument.type}-document`);
this.warnings = Array.from(base.warnings);
}
get url() {
return this._scannedDocument.url;
}
get isInline() {
return this._scannedDocument.isInline;
}
get sourceRange() {
return this._scannedDocument.sourceRange;
}
get astNode() {
return this._scannedDocument.astNode;
}
get parsedDocument() {
return this._scannedDocument.document;
}
get resolved() {
return this._doneResolving;
}
get type() {
return this.parsedDocument.type;
}
/**
* Resolves all features of this document, so that they have references to all
* their dependencies.
*
* This method can only be called once
*/
// TODO(justinfagnani): move to ScannedDocument
resolve() {
if (this._doneResolving) {
throw new Error('resolve can only be called once');
}
if (this._begunResolving) {
return;
}
this._begunResolving = true;
this._addFeature(this);
for (const scannedFeature of this._scannedDocument.features) {
if (resolvable_1.isResolvable(scannedFeature)) {
const feature = scannedFeature.resolve(this);
if (feature) {
this._addFeature(feature);
}
}
}
this._doneResolving = true;
}
/**
* Adds and indexes a feature to this document before resolve().
*/
_addFeature(feature) {
if (this._doneResolving) {
throw new Error('_addFeature can not be called after _resolve()');
}
this._indexFeature(feature);
this._localFeatures.add(feature);
if (isDeclaredWithStatement(feature) &&
feature.statementAst !== undefined) {
this._localFeaturesByStatement.get(feature.statementAst).add(feature);
}
}
getFeatures(query = {}) {
if (query.statement !== undefined) {
return this._getByStatement(query.statement, query.kind);
}
if (query.id && query.kind) {
return this._getById(query.kind, query.id, query);
}
else if (query.kind) {
return this._getByKind(query.kind, query);
}
const features = new Set();
this._listFeatures(features, new Set(), query);
const queryId = query.id;
if (queryId) {
const filteredFeatures = Array.from(features).filter((f) => f.identifiers.has(queryId));
return new Set(filteredFeatures);
}
return features;
}
_getByKind(kind, query = {}) {
if (this._featuresByKind && this._isCachable(query)) {
// We have a fast index! Use that.
const features = this._featuresByKind.get(kind) || new Set();
if (!query.externalPackages) {
return this._filterOutExternal(features);
}
return features;
}
else if (this._doneResolving && this._isCachable(query)) {
// We're done discovering features in this document and its children so
// we can safely build up the indexes.
this._buildIndexes();
return this._getByKind(kind, query);
}
return this._getSlowlyByKind(kind, query);
}
_getById(kind, identifier, query = {}) {
if (this._featuresByKindAndId && this._isCachable(query)) {
// We have a fast index! Use that.
const idMap = this._featuresByKindAndId.get(kind);
const features = (idMap && idMap.get(identifier)) || new Set();
if (!query.externalPackages) {
return this._filterOutExternal(features);
}
return features;
}
else if (this._doneResolving && this._isCachable(query)) {
// We're done discovering features in this document and its children so
// we can safely build up the indexes.
this._buildIndexes();
return this._getById(kind, identifier, query);
}
const result = new Set();
for (const featureOfKind of this._getByKind(kind, query)) {
if (featureOfKind.identifiers.has(identifier)) {
result.add(featureOfKind);
}
}
return result;
}
_getByStatement(statement, kind) {
const result = this._localFeaturesByStatement.get(statement);
if (kind === undefined) {
return result;
}
return this._filterByKind(result, kind);
}
/** Filters out the given features by the given kind. */
_filterByKind(features, kind) {
const result = new Set();
for (const feature of features) {
if (feature.kinds.has(kind)) {
result.add(feature);
}
}
return result;
}
_isCachable(query = {}) {
return !!query.imported && !query.noLazyImports &&
!query.excludeBackreferences;
}
_getSlowlyByKind(kind, query) {
const allFeatures = new Set();
this._listFeatures(allFeatures, new Set(), query);
const result = new Set();
for (const feature of allFeatures) {
if (feature.kinds.has(kind)) {
result.add(feature);
}
}
return result;
}
/**
* Walks the graph of documents, starting from `this`, finding features which
* match the given query and adding them to the `result` set. Uses `visited`
* to deal with cycles.
*
* This method is O(numFeatures), though it does not walk documents that are
* not relevant to the query (e.g. based on whether the query follows imports,
* or excludes lazy imports)
*/
_listFeatures(result, visited, query) {
if (visited.has(this)) {
return;
}
visited.add(this);
for (const feature of this._localFeatures) {
// Don't include a DocumentBackreference feature in the result set if the
// query excludes them.
if (!feature.kinds.has('document-backreference') ||
!query.excludeBackreferences) {
result.add(feature);
}
if (feature.kinds.has('document-backreference') &&
!query.excludeBackreferences) {
const containerDocument = feature.document;
result.add(containerDocument);
containerDocument._listFeatures(result, visited, query);
}
if (feature.kinds.has('document')) {
feature._listFeatures(result, visited, query);
}
if (feature.kinds.has('import') && query.imported) {
const imprt = feature;
const isPackageInternal = imprt.document && !analysis_1.Analysis.isExternal(imprt.url);
const externalityOk = query.externalPackages || isPackageInternal;
const lazinessOk = !query.noLazyImports || !imprt.lazy;
if (imprt.document !== undefined && externalityOk && lazinessOk) {
imprt.document._listFeatures(result, visited, query);
}
}
}
}
_filterOutExternal(features) {
const result = new Set();
for (const feature of features) {
if (feature.sourceRange &&
analysis_1.Analysis.isExternal(feature.sourceRange.file)) {
continue;
}
result.add(feature);
}
return result;
}
/**
* Get warnings for the document and all matched features.
*/
getWarnings(query = {}) {
const warnings = new Set(this.warnings);
for (const feature of this.getFeatures(query)) {
for (const warning of feature.warnings) {
warnings.add(warning);
}
}
return Array.from(warnings);
}
toString() {
return this._toString(new Set()).join('\n');
}
_toString(documentsWalked) {
let result = [`<Document type=${this.parsedDocument.type} url=${this.url}>\n`];
if (documentsWalked.has(this)) {
return result;
}
documentsWalked.add(this);
for (const localFeature of this._localFeatures) {
if (localFeature instanceof Document) {
result = result.concat(localFeature._toString(documentsWalked).map((line) => ` ${line}`));
}
else {
let subResult = localFeature.toString();
if (subResult === '[object Object]') {
subResult = `<${localFeature.constructor.name} kinds="${Array.from(localFeature.kinds).join(', ')}" ids="${Array.from(localFeature.identifiers).join(',')}">}`;
}
result.push(` ${subResult}`);
}
}
return result;
}
stringify() {
const inlineDocuments = Array.from(this._localFeatures)
.filter((f) => f instanceof Document && f.isInline)
.map((d) => d.parsedDocument);
return this.parsedDocument.stringify({ inlineDocuments: inlineDocuments });
}
_indexFeature(feature) {
if (!this._featuresByKind || !this._featuresByKindAndId) {
return;
}
for (const kind of feature.kinds) {
const kindSet = this._featuresByKind.get(kind) || new Set();
kindSet.add(feature);
this._featuresByKind.set(kind, kindSet);
for (const id of feature.identifiers) {
const identifiersMap = this._featuresByKindAndId.get(kind) ||
new Map();
this._featuresByKindAndId.set(kind, identifiersMap);
const idSet = identifiersMap.get(id) || new Set();
identifiersMap.set(id, idSet);
idSet.add(feature);
}
}
}
_buildIndexes() {
if (this._featuresByKind) {
throw new Error('Tried to build indexes multiple times. This should never happen.');
}
if (!this._doneResolving) {
throw new Error(`Tried to build indexes before finished resolving. ` +
`Need to wait until afterwards or the indexes would be incomplete.`);
}
this._featuresByKind = new Map();
this._featuresByKindAndId = new Map();
for (const feature of this.getFeatures({ imported: true, externalPackages: true })) {
this._indexFeature(feature);
}
}
}
exports.Document = Document;
//# sourceMappingURL=document.js.map