react / wstein / node_modules / react / node_modules / envify / node_modules / jstransform / src / utils.js
80542 views/**1* Copyright 2013 Facebook, Inc.2*3* Licensed under the Apache License, Version 2.0 (the "License");4* you may not use this file except in compliance with the License.5* You may obtain a copy of the License at6*7* http://www.apache.org/licenses/LICENSE-2.08*9* Unless required by applicable law or agreed to in writing, software10* distributed under the License is distributed on an "AS IS" BASIS,11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12* See the License for the specific language governing permissions and13* limitations under the License.14*/151617/*jslint node: true*/18var Syntax = require('esprima-fb').Syntax;19var leadingIndentRegexp = /(^|\n)( {2}|\t)/g;20var nonWhiteRegexp = /(\S)/g;2122/**23* A `state` object represents the state of the parser. It has "local" and24* "global" parts. Global contains parser position, source, etc. Local contains25* scope based properties like current class name. State should contain all the26* info required for transformation. It's the only mandatory object that is27* being passed to every function in transform chain.28*29* @param {string} source30* @param {object} transformOptions31* @return {object}32*/33function createState(source, rootNode, transformOptions) {34return {35/**36* A tree representing the current local scope (and its lexical scope chain)37* Useful for tracking identifiers from parent scopes, etc.38* @type {Object}39*/40localScope: {41parentNode: rootNode,42parentScope: null,43identifiers: {},44tempVarIndex: 0,45tempVars: []46},47/**48* The name (and, if applicable, expression) of the super class49* @type {Object}50*/51superClass: null,52/**53* The namespace to use when munging identifiers54* @type {String}55*/56mungeNamespace: '',57/**58* Ref to the node for the current MethodDefinition59* @type {Object}60*/61methodNode: null,62/**63* Ref to the node for the FunctionExpression of the enclosing64* MethodDefinition65* @type {Object}66*/67methodFuncNode: null,68/**69* Name of the enclosing class70* @type {String}71*/72className: null,73/**74* Whether we're currently within a `strict` scope75* @type {Bool}76*/77scopeIsStrict: null,78/**79* Indentation offset80* @type {Number}81*/82indentBy: 0,83/**84* Global state (not affected by updateState)85* @type {Object}86*/87g: {88/**89* A set of general options that transformations can consider while doing90* a transformation:91*92* - minify93* Specifies that transformation steps should do their best to minify94* the output source when possible. This is useful for places where95* minification optimizations are possible with higher-level context96* info than what jsxmin can provide.97*98* For example, the ES6 class transform will minify munged private99* variables if this flag is set.100*/101opts: transformOptions,102/**103* Current position in the source code104* @type {Number}105*/106position: 0,107/**108* Auxiliary data to be returned by transforms109* @type {Object}110*/111extra: {},112/**113* Buffer containing the result114* @type {String}115*/116buffer: '',117/**118* Source that is being transformed119* @type {String}120*/121source: source,122123/**124* Cached parsed docblock (see getDocblock)125* @type {object}126*/127docblock: null,128129/**130* Whether the thing was used131* @type {Boolean}132*/133tagNamespaceUsed: false,134135/**136* If using bolt xjs transformation137* @type {Boolean}138*/139isBolt: undefined,140141/**142* Whether to record source map (expensive) or not143* @type {SourceMapGenerator|null}144*/145sourceMap: null,146147/**148* Filename of the file being processed. Will be returned as a source149* attribute in the source map150*/151sourceMapFilename: 'source.js',152153/**154* Only when source map is used: last line in the source for which155* source map was generated156* @type {Number}157*/158sourceLine: 1,159160/**161* Only when source map is used: last line in the buffer for which162* source map was generated163* @type {Number}164*/165bufferLine: 1,166167/**168* The top-level Program AST for the original file.169*/170originalProgramAST: null,171172sourceColumn: 0,173bufferColumn: 0174}175};176}177178/**179* Updates a copy of a given state with "update" and returns an updated state.180*181* @param {object} state182* @param {object} update183* @return {object}184*/185function updateState(state, update) {186var ret = Object.create(state);187Object.keys(update).forEach(function(updatedKey) {188ret[updatedKey] = update[updatedKey];189});190return ret;191}192193/**194* Given a state fill the resulting buffer from the original source up to195* the end196*197* @param {number} end198* @param {object} state199* @param {?function} contentTransformer Optional callback to transform newly200* added content.201*/202function catchup(end, state, contentTransformer) {203if (end < state.g.position) {204// cannot move backwards205return;206}207var source = state.g.source.substring(state.g.position, end);208var transformed = updateIndent(source, state);209if (state.g.sourceMap && transformed) {210// record where we are211state.g.sourceMap.addMapping({212generated: { line: state.g.bufferLine, column: state.g.bufferColumn },213original: { line: state.g.sourceLine, column: state.g.sourceColumn },214source: state.g.sourceMapFilename215});216217// record line breaks in transformed source218var sourceLines = source.split('\n');219var transformedLines = transformed.split('\n');220// Add line break mappings between last known mapping and the end of the221// added piece. So for the code piece222// (foo, bar);223// > var x = 2;224// > var b = 3;225// var c =226// only add lines marked with ">": 2, 3.227for (var i = 1; i < sourceLines.length - 1; i++) {228state.g.sourceMap.addMapping({229generated: { line: state.g.bufferLine, column: 0 },230original: { line: state.g.sourceLine, column: 0 },231source: state.g.sourceMapFilename232});233state.g.sourceLine++;234state.g.bufferLine++;235}236// offset for the last piece237if (sourceLines.length > 1) {238state.g.sourceLine++;239state.g.bufferLine++;240state.g.sourceColumn = 0;241state.g.bufferColumn = 0;242}243state.g.sourceColumn += sourceLines[sourceLines.length - 1].length;244state.g.bufferColumn +=245transformedLines[transformedLines.length - 1].length;246}247state.g.buffer +=248contentTransformer ? contentTransformer(transformed) : transformed;249state.g.position = end;250}251252/**253* Returns original source for an AST node.254* @param {object} node255* @param {object} state256* @return {string}257*/258function getNodeSourceText(node, state) {259return state.g.source.substring(node.range[0], node.range[1]);260}261262function _replaceNonWhite(value) {263return value.replace(nonWhiteRegexp, ' ');264}265266/**267* Removes all non-whitespace characters268*/269function _stripNonWhite(value) {270return value.replace(nonWhiteRegexp, '');271}272273/**274* Finds the position of the next instance of the specified syntactic char in275* the pending source.276*277* NOTE: This will skip instances of the specified char if they sit inside a278* comment body.279*280* NOTE: This function also assumes that the buffer's current position is not281* already within a comment or a string. This is rarely the case since all282* of the buffer-advancement utility methods tend to be used on syntactic283* nodes' range values -- but it's a small gotcha that's worth mentioning.284*/285function getNextSyntacticCharOffset(char, state) {286var pendingSource = state.g.source.substring(state.g.position);287var pendingSourceLines = pendingSource.split('\n');288289var charOffset = 0;290var line;291var withinBlockComment = false;292var withinString = false;293lineLoop: while ((line = pendingSourceLines.shift()) !== undefined) {294var lineEndPos = charOffset + line.length;295charLoop: for (; charOffset < lineEndPos; charOffset++) {296var currChar = pendingSource[charOffset];297if (currChar === '"' || currChar === '\'') {298withinString = !withinString;299continue charLoop;300} else if (withinString) {301continue charLoop;302} else if (charOffset + 1 < lineEndPos) {303var nextTwoChars = currChar + line[charOffset + 1];304if (nextTwoChars === '//') {305charOffset = lineEndPos + 1;306continue lineLoop;307} else if (nextTwoChars === '/*') {308withinBlockComment = true;309charOffset += 1;310continue charLoop;311} else if (nextTwoChars === '*/') {312withinBlockComment = false;313charOffset += 1;314continue charLoop;315}316}317318if (!withinBlockComment && currChar === char) {319return charOffset + state.g.position;320}321}322323// Account for '\n'324charOffset++;325withinString = false;326}327328throw new Error('`' + char + '` not found!');329}330331/**332* Catches up as `catchup` but replaces non-whitespace chars with spaces.333*/334function catchupWhiteOut(end, state) {335catchup(end, state, _replaceNonWhite);336}337338/**339* Catches up as `catchup` but removes all non-whitespace characters.340*/341function catchupWhiteSpace(end, state) {342catchup(end, state, _stripNonWhite);343}344345/**346* Removes all non-newline characters347*/348var reNonNewline = /[^\n]/g;349function stripNonNewline(value) {350return value.replace(reNonNewline, function() {351return '';352});353}354355/**356* Catches up as `catchup` but removes all non-newline characters.357*358* Equivalent to appending as many newlines as there are in the original source359* between the current position and `end`.360*/361function catchupNewlines(end, state) {362catchup(end, state, stripNonNewline);363}364365366/**367* Same as catchup but does not touch the buffer368*369* @param {number} end370* @param {object} state371*/372function move(end, state) {373// move the internal cursors374if (state.g.sourceMap) {375if (end < state.g.position) {376state.g.position = 0;377state.g.sourceLine = 1;378state.g.sourceColumn = 0;379}380381var source = state.g.source.substring(state.g.position, end);382var sourceLines = source.split('\n');383if (sourceLines.length > 1) {384state.g.sourceLine += sourceLines.length - 1;385state.g.sourceColumn = 0;386}387state.g.sourceColumn += sourceLines[sourceLines.length - 1].length;388}389state.g.position = end;390}391392/**393* Appends a string of text to the buffer394*395* @param {string} str396* @param {object} state397*/398function append(str, state) {399if (state.g.sourceMap && str) {400state.g.sourceMap.addMapping({401generated: { line: state.g.bufferLine, column: state.g.bufferColumn },402original: { line: state.g.sourceLine, column: state.g.sourceColumn },403source: state.g.sourceMapFilename404});405var transformedLines = str.split('\n');406if (transformedLines.length > 1) {407state.g.bufferLine += transformedLines.length - 1;408state.g.bufferColumn = 0;409}410state.g.bufferColumn +=411transformedLines[transformedLines.length - 1].length;412}413state.g.buffer += str;414}415416/**417* Update indent using state.indentBy property. Indent is measured in418* double spaces. Updates a single line only.419*420* @param {string} str421* @param {object} state422* @return {string}423*/424function updateIndent(str, state) {425/*jshint -W004*/426var indentBy = state.indentBy;427if (indentBy < 0) {428for (var i = 0; i < -indentBy; i++) {429str = str.replace(leadingIndentRegexp, '$1');430}431} else {432for (var i = 0; i < indentBy; i++) {433str = str.replace(leadingIndentRegexp, '$1$2$2');434}435}436return str;437}438439/**440* Calculates indent from the beginning of the line until "start" or the first441* character before start.442* @example443* " foo.bar()"444* ^445* start446* indent will be " "447*448* @param {number} start449* @param {object} state450* @return {string}451*/452function indentBefore(start, state) {453var end = start;454start = start - 1;455456while (start > 0 && state.g.source[start] != '\n') {457if (!state.g.source[start].match(/[ \t]/)) {458end = start;459}460start--;461}462return state.g.source.substring(start + 1, end);463}464465function getDocblock(state) {466if (!state.g.docblock) {467var docblock = require('./docblock');468state.g.docblock =469docblock.parseAsObject(docblock.extract(state.g.source));470}471return state.g.docblock;472}473474function identWithinLexicalScope(identName, state, stopBeforeNode) {475var currScope = state.localScope;476while (currScope) {477if (currScope.identifiers[identName] !== undefined) {478return true;479}480481if (stopBeforeNode && currScope.parentNode === stopBeforeNode) {482break;483}484485currScope = currScope.parentScope;486}487return false;488}489490function identInLocalScope(identName, state) {491return state.localScope.identifiers[identName] !== undefined;492}493494/**495* @param {object} boundaryNode496* @param {?array} path497* @return {?object} node498*/499function initScopeMetadata(boundaryNode, path, node) {500return {501boundaryNode: boundaryNode,502bindingPath: path,503bindingNode: node504};505}506507function declareIdentInLocalScope(identName, metaData, state) {508state.localScope.identifiers[identName] = {509boundaryNode: metaData.boundaryNode,510path: metaData.bindingPath,511node: metaData.bindingNode,512state: Object.create(state)513};514}515516function getLexicalBindingMetadata(identName, state) {517var currScope = state.localScope;518while (currScope) {519if (currScope.identifiers[identName] !== undefined) {520return currScope.identifiers[identName];521}522523currScope = currScope.parentScope;524}525}526527function getLocalBindingMetadata(identName, state) {528return state.localScope.identifiers[identName];529}530531/**532* Apply the given analyzer function to the current node. If the analyzer533* doesn't return false, traverse each child of the current node using the given534* traverser function.535*536* @param {function} analyzer537* @param {function} traverser538* @param {object} node539* @param {array} path540* @param {object} state541*/542function analyzeAndTraverse(analyzer, traverser, node, path, state) {543if (node.type) {544if (analyzer(node, path, state) === false) {545return;546}547path.unshift(node);548}549550getOrderedChildren(node).forEach(function(child) {551traverser(child, path, state);552});553554node.type && path.shift();555}556557/**558* It is crucial that we traverse in order, or else catchup() on a later559* node that is processed out of order can move the buffer past a node560* that we haven't handled yet, preventing us from modifying that node.561*562* This can happen when a node has multiple properties containing children.563* For example, XJSElement nodes have `openingElement`, `closingElement` and564* `children`. If we traverse `openingElement`, then `closingElement`, then565* when we get to `children`, the buffer has already caught up to the end of566* the closing element, after the children.567*568* This is basically a Schwartzian transform. Collects an array of children,569* each one represented as [child, startIndex]; sorts the array by start570* index; then traverses the children in that order.571*/572function getOrderedChildren(node) {573var queue = [];574for (var key in node) {575if (node.hasOwnProperty(key)) {576enqueueNodeWithStartIndex(queue, node[key]);577}578}579queue.sort(function(a, b) { return a[1] - b[1]; });580return queue.map(function(pair) { return pair[0]; });581}582583/**584* Helper function for analyzeAndTraverse which queues up all of the children585* of the given node.586*587* Children can also be found in arrays, so we basically want to merge all of588* those arrays together so we can sort them and then traverse the children589* in order.590*591* One example is the Program node. It contains `body` and `comments`, both592* arrays. Lexographically, comments are interspersed throughout the body593* nodes, but esprima's AST groups them together.594*/595function enqueueNodeWithStartIndex(queue, node) {596if (typeof node !== 'object' || node === null) {597return;598}599if (node.range) {600queue.push([node, node.range[0]]);601} else if (Array.isArray(node)) {602for (var ii = 0; ii < node.length; ii++) {603enqueueNodeWithStartIndex(queue, node[ii]);604}605}606}607608/**609* Checks whether a node or any of its sub-nodes contains610* a syntactic construct of the passed type.611* @param {object} node - AST node to test.612* @param {string} type - node type to lookup.613*/614function containsChildOfType(node, type) {615return containsChildMatching(node, function(node) {616return node.type === type;617});618}619620function containsChildMatching(node, matcher) {621var foundMatchingChild = false;622function nodeTypeAnalyzer(node) {623if (matcher(node) === true) {624foundMatchingChild = true;625return false;626}627}628function nodeTypeTraverser(child, path, state) {629if (!foundMatchingChild) {630foundMatchingChild = containsChildMatching(child, matcher);631}632}633analyzeAndTraverse(634nodeTypeAnalyzer,635nodeTypeTraverser,636node,637[]638);639return foundMatchingChild;640}641642var scopeTypes = {};643scopeTypes[Syntax.ArrowFunctionExpression] = true;644scopeTypes[Syntax.FunctionExpression] = true;645scopeTypes[Syntax.FunctionDeclaration] = true;646scopeTypes[Syntax.Program] = true;647648function getBoundaryNode(path) {649for (var ii = 0; ii < path.length; ++ii) {650if (scopeTypes[path[ii].type]) {651return path[ii];652}653}654throw new Error(655'Expected to find a node with one of the following types in path:\n' +656JSON.stringify(Object.keys(scopeTypes))657);658}659660function getTempVar(tempVarIndex) {661return '$__' + tempVarIndex;662}663664function injectTempVar(state) {665var tempVar = '$__' + (state.localScope.tempVarIndex++);666state.localScope.tempVars.push(tempVar);667return tempVar;668}669670function injectTempVarDeclarations(state, index) {671if (state.localScope.tempVars.length) {672state.g.buffer =673state.g.buffer.slice(0, index) +674'var ' + state.localScope.tempVars.join(', ') + ';' +675state.g.buffer.slice(index);676state.localScope.tempVars = [];677}678}679680exports.analyzeAndTraverse = analyzeAndTraverse;681exports.append = append;682exports.catchup = catchup;683exports.catchupNewlines = catchupNewlines;684exports.catchupWhiteOut = catchupWhiteOut;685exports.catchupWhiteSpace = catchupWhiteSpace;686exports.containsChildMatching = containsChildMatching;687exports.containsChildOfType = containsChildOfType;688exports.createState = createState;689exports.declareIdentInLocalScope = declareIdentInLocalScope;690exports.getBoundaryNode = getBoundaryNode;691exports.getDocblock = getDocblock;692exports.getLexicalBindingMetadata = getLexicalBindingMetadata;693exports.getLocalBindingMetadata = getLocalBindingMetadata;694exports.getNextSyntacticCharOffset = getNextSyntacticCharOffset;695exports.getNodeSourceText = getNodeSourceText;696exports.getOrderedChildren = getOrderedChildren;697exports.getTempVar = getTempVar;698exports.identInLocalScope = identInLocalScope;699exports.identWithinLexicalScope = identWithinLexicalScope;700exports.indentBefore = indentBefore;701exports.initScopeMetadata = initScopeMetadata;702exports.injectTempVar = injectTempVar;703exports.injectTempVarDeclarations = injectTempVarDeclarations;704exports.move = move;705exports.scopeTypes = scopeTypes;706exports.updateIndent = updateIndent;707exports.updateState = updateState;708709710