UNPKG

@locker/ast-lib-maker

Version:

Lightning Web Security AST library maker utilities

222 lines (221 loc) 7.03 kB
/*! * Copyright (C) 2020 salesforce.com, inc. */ import { ObjectAssign } from '@locker/shared'; const ASTERISK_CHARACTER = '*'; const DOCUMENT_ALIASES = [`${ASTERISK_CHARACTER}.ownerDocument`, 'document']; const WINDOW_ALIASES = ['document.defaultView', 'frames', 'globalThis', 'parent', 'self', 'top', 'window']; function dehydratedPatternStartsWith(dehydratedPattern, searchString) { return dehydratedPattern.startsWith(`${searchString}.`); } function dehydratedPatternStartsWithByAliases(dehydratedPattern, aliases) { for (let i = 0, { length } = aliases; i < length; i += 1) { if (dehydratedPatternStartsWith(dehydratedPattern, aliases[i])) { return true; } } return false; } function dehydratedPatternTail(dehydratedPattern) { return dehydratedPattern.slice(dehydratedPattern.indexOf('.') + 1); } function dehydratePattern(pattern) { return pattern.join('.'); } function expandPattern(dehydratedPattern) { const expandedPatterns = []; // Exit early for patterns starting with '*.'. if (dehydratedPatternStartsWith(dehydratedPattern, ASTERISK_CHARACTER)) { return expandedPatterns; } const dehydratedPatterns = [dehydratedPattern]; const push = otherDehydratedPattern => { if (!dehydratedPatterns.includes(otherDehydratedPattern)) { dehydratedPatterns.push(otherDehydratedPattern); expandedPatterns.push(rehydratePattern(otherDehydratedPattern)); } }; const pushAliases = (aliases, pathTail) => { for (let i = 0, { length } = aliases; i < length; i += 1) { push(`${aliases[i]}.${pathTail}`); } }; const topLevelPath = isWindowStringPattern(dehydratedPattern) ? dehydratedPatternTail(dehydratedPattern) : dehydratedPattern; // Expand patterns with a top-level identifier. push(topLevelPath); // Expand patterns with other `window` aliases. pushAliases(WINDOW_ALIASES, topLevelPath); // Expand patterns with other `document` aliases. if (isDocumentStringPattern(topLevelPath)) { pushAliases(DOCUMENT_ALIASES, dehydratedPatternTail(topLevelPath)); } return expandedPatterns; } function isDocumentStringPattern(dehydratedPattern) { return dehydratedPatternStartsWithByAliases(dehydratedPattern, DOCUMENT_ALIASES); } function isWindowStringPattern(dehydratedPattern) { return dehydratedPatternStartsWithByAliases(dehydratedPattern, WINDOW_ALIASES); } function rehydratePattern(dehydratedPattern) { return dehydratedPattern.split('.'); } const baseLib = { /** * String patterns are converted to arrays and expanded for document and * window aliases. For example: * ```js * ['window.top'] * ``` * expands to: * ```js * [ * ['window', 'top'], * ['top'], * ['document', 'defaultView', 'top'], * ['frames', 'top'], * ['globalThis', 'top'], * ['self', 'top'] * ] * ``` */ expandPatterns(dehydratedPatterns) { const expandedPatterns = []; for (let i = 0, { length } = dehydratedPatterns; i < length; i += 1) { expandedPatterns.push(rehydratePattern(dehydratedPatterns[i])); } for (let i = 0, { length } = dehydratedPatterns; i < length; i += 1) { const patterns = expandPattern(dehydratedPatterns[i]); // eslint-disable-next-line @typescript-eslint/naming-convention for (let j = 0, { length: length_j } = patterns; j < length_j; j += 1) { expandedPatterns.push(patterns[j]); } } return expandedPatterns; }, getIdentifierName(node) { return node.name; }, getParent(node) { return node.parent; }, getType(node) { return node.type; }, isMatchableNode(node) { if (node) { const type = this.getType(node); return type === 'Identifier' || type === 'ThisExpression'; } return false; }, isNodeByAsteriskOrName(node, name) { if (this.isMatchableNode(node)) { return name === ASTERISK_CHARACTER || name === this.getIdentifierName(node) || this.getType(node) === 'ThisExpression'; } return false; }, isNonComputedMemberExpressionNode(node) { if (node) { return this.getType(node) === 'MemberExpression' && !node.computed; } return false; }, match(identifier, patterns) { expandedLoop: for (let i = 0, { length } = patterns; i < length; i += 1) { const pattern = patterns[i]; // Skip fast for mismatched identifiers. if (!this.isNodeByAsteriskOrName(identifier, pattern[0])) { continue; } // Match and exit early for non-member identifier matches. if (pattern.length === 1) { return { identifier, node: identifier, pattern: dehydratePattern(pattern) }; } let currentNode = identifier; // eslint-disable-next-line @typescript-eslint/naming-convention for (let j = 0, { length: length_j } = pattern; j < length_j; j += 1) { currentNode = this.getParent(currentNode); // Skip for unexpected nodes. if (!this.isNonComputedMemberExpressionNode(currentNode)) { continue expandedLoop; } const { object, property } = currentNode; let matchedByPreviousAsterisk = false; if (this.isMatchableNode(object)) { // Skip for mismatched object identifiers. if (!this.isNodeByAsteriskOrName(object, pattern[j])) { matchedByPreviousAsterisk = pattern[j - 1] === ASTERISK_CHARACTER; // istanbul ignore else: currently unreachable via tests if (!matchedByPreviousAsterisk) { continue expandedLoop; } } // istanbul ignore else: currently unreachable via tests if (!matchedByPreviousAsterisk) { j += 1; } } // Skip nodes that aren't part of a property chain. else if (!this.isNonComputedMemberExpressionNode(object)) { continue expandedLoop; } // Skip for mismatched property identifiers. if (!this.isNodeByAsteriskOrName(property, pattern[j])) { matchedByPreviousAsterisk = pattern[j - 1] === ASTERISK_CHARACTER; if (!matchedByPreviousAsterisk) { continue expandedLoop; } } if (matchedByPreviousAsterisk) { j -= 1; } } // If we've made it this far it's a match! return { identifier, node: currentNode, pattern: dehydratePattern(pattern) }; } return undefined; }, matchAll(identifiers, patterns) { const matches = []; for (let i = 0, { length } = identifiers; i < length; i += 1) { const matchData = this.match(identifiers[i], patterns); if (matchData) { matches.push(matchData); } } return matches; } }; function createLib(overrides) { return ObjectAssign({}, baseLib, overrides); } export { createLib }; /*! version: 0.25.3 */