/** * Copyright 2013 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /*jslint node: true*/ /** * A `state` object represents the state of the parser. It has "local" and * "global" parts. Global contains parser position, source, etc. Local contains * scope based properties like current class name. State should contain all the * info required for transformation. It's the only mandatory object that is * being passed to every function in transform chain. * * @param {string} source * @param {object} transformOptions * @return {object} */ function createState(source, rootNode, transformOptions) { return { /** * A tree representing the current local scope (and its lexical scope chain) * Useful for tracking identifiers from parent scopes, etc. * @type {Object} */ localScope: { parentNode: rootNode, parentScope: null, identifiers: {} }, /** * The name (and, if applicable, expression) of the super class * @type {Object} */ superClass: null, /** * The namespace to use when munging identifiers * @type {String} */ mungeNamespace: '', /** * Ref to the node for the FunctionExpression of the enclosing * MethodDefinition * @type {Object} */ methodFuncNode: null, /** * Name of the enclosing class * @type {String} */ className: null, /** * Whether we're currently within a `strict` scope * @type {Bool} */ scopeIsStrict: null, /** * Global state (not affected by updateState) * @type {Object} */ g: { /** * A set of general options that transformations can consider while doing * a transformation: * * - minify * Specifies that transformation steps should do their best to minify * the output source when possible. This is useful for places where * minification optimizations are possible with higher-level context * info than what jsxmin can provide. * * For example, the ES6 class transform will minify munged private * variables if this flag is set. */ opts: transformOptions, /** * Current position in the source code * @type {Number} */ position: 0, /** * Buffer containing the result * @type {String} */ buffer: '', /** * Indentation offset (only negative offset is supported now) * @type {Number} */ indentBy: 0, /** * Source that is being transformed * @type {String} */ source: source, /** * Cached parsed docblock (see getDocblock) * @type {object} */ docblock: null, /** * Whether the thing was used * @type {Boolean} */ tagNamespaceUsed: false, /** * If using bolt xjs transformation * @type {Boolean} */ isBolt: undefined, /** * Whether to record source map (expensive) or not * @type {SourceMapGenerator|null} */ sourceMap: null, /** * Filename of the file being processed. Will be returned as a source * attribute in the source map */ sourceMapFilename: 'source.js', /** * Only when source map is used: last line in the source for which * source map was generated * @type {Number} */ sourceLine: 1, /** * Only when source map is used: last line in the buffer for which * source map was generated * @type {Number} */ bufferLine: 1, /** * The top-level Program AST for the original file. */ originalProgramAST: null, sourceColumn: 0, bufferColumn: 0 } }; } /** * Updates a copy of a given state with "update" and returns an updated state. * * @param {object} state * @param {object} update * @return {object} */ function updateState(state, update) { var ret = Object.create(state); Object.keys(update).forEach(function(updatedKey) { ret[updatedKey] = update[updatedKey]; }); return ret; } /** * Given a state fill the resulting buffer from the original source up to * the end * * @param {number} end * @param {object} state * @param {?function} contentTransformer Optional callback to transform newly * added content. */ function catchup(end, state, contentTransformer) { if (end < state.g.position) { // cannot move backwards return; } var source = state.g.source.substring(state.g.position, end); var transformed = updateIndent(source, state); if (state.g.sourceMap && transformed) { // record where we are state.g.sourceMap.addMapping({ generated: { line: state.g.bufferLine, column: state.g.bufferColumn }, original: { line: state.g.sourceLine, column: state.g.sourceColumn }, source: state.g.sourceMapFilename }); // record line breaks in transformed source var sourceLines = source.split('\n'); var transformedLines = transformed.split('\n'); // Add line break mappings between last known mapping and the end of the // added piece. So for the code piece // (foo, bar); // > var x = 2; // > var b = 3; // var c = // only add lines marked with ">": 2, 3. for (var i = 1; i < sourceLines.length - 1; i++) { state.g.sourceMap.addMapping({ generated: { line: state.g.bufferLine, column: 0 }, original: { line: state.g.sourceLine, column: 0 }, source: state.g.sourceMapFilename }); state.g.sourceLine++; state.g.bufferLine++; } // offset for the last piece if (sourceLines.length > 1) { state.g.sourceLine++; state.g.bufferLine++; state.g.sourceColumn = 0; state.g.bufferColumn = 0; } state.g.sourceColumn += sourceLines[sourceLines.length - 1].length; state.g.bufferColumn += transformedLines[transformedLines.length - 1].length; } state.g.buffer += contentTransformer ? contentTransformer(transformed) : transformed; state.g.position = end; } /** * Removes all non-whitespace characters */ var reNonWhite = /(\S)/g; function stripNonWhite(value) { return value.replace(reNonWhite, function() { return ''; }); } /** * Catches up as `catchup` but removes all non-whitespace characters. */ function catchupWhiteSpace(end, state) { catchup(end, state, stripNonWhite); } /** * Removes all non-newline characters */ var reNonNewline = /[^\n]/g; function stripNonNewline(value) { return value.replace(reNonNewline, function() { return ''; }); } /** * Catches up as `catchup` but removes all non-newline characters. * * Equivalent to appending as many newlines as there are in the original source * between the current position and `end`. */ function catchupNewlines(end, state) { catchup(end, state, stripNonNewline); } /** * Same as catchup but does not touch the buffer * * @param {number} end * @param {object} state */ function move(end, state) { // move the internal cursors if (state.g.sourceMap) { if (end < state.g.position) { state.g.position = 0; state.g.sourceLine = 1; state.g.sourceColumn = 0; } var source = state.g.source.substring(state.g.position, end); var sourceLines = source.split('\n'); if (sourceLines.length > 1) { state.g.sourceLine += sourceLines.length - 1; state.g.sourceColumn = 0; } state.g.sourceColumn += sourceLines[sourceLines.length - 1].length; } state.g.position = end; } /** * Appends a string of text to the buffer * * @param {string} str * @param {object} state */ function append(str, state) { if (state.g.sourceMap && str) { state.g.sourceMap.addMapping({ generated: { line: state.g.bufferLine, column: state.g.bufferColumn }, original: { line: state.g.sourceLine, column: state.g.sourceColumn }, source: state.g.sourceMapFilename }); var transformedLines = str.split('\n'); if (transformedLines.length > 1) { state.g.bufferLine += transformedLines.length - 1; state.g.bufferColumn = 0; } state.g.bufferColumn += transformedLines[transformedLines.length - 1].length; } state.g.buffer += str; } /** * Update indent using state.indentBy property. Indent is measured in * double spaces. Updates a single line only. * * @param {string} str * @param {object} state * @return {string} */ function updateIndent(str, state) { for (var i = 0; i < -state.g.indentBy; i++) { str = str.replace(/(^|\n)( {2}|\t)/g, '$1'); } return str; } /** * Calculates indent from the beginning of the line until "start" or the first * character before start. * @example * " foo.bar()" * ^ * start * indent will be 2 * * @param {number} start * @param {object} state * @return {number} */ function indentBefore(start, state) { var end = start; start = start - 1; while (start > 0 && state.g.source[start] != '\n') { if (!state.g.source[start].match(/[ \t]/)) { end = start; } start--; } return state.g.source.substring(start + 1, end); } function getDocblock(state) { if (!state.g.docblock) { var docblock = require('./docblock'); state.g.docblock = docblock.parseAsObject(docblock.extract(state.g.source)); } return state.g.docblock; } function identWithinLexicalScope(identName, state, stopBeforeNode) { var currScope = state.localScope; while (currScope) { if (currScope.identifiers[identName] !== undefined) { return true; } if (stopBeforeNode && currScope.parentNode === stopBeforeNode) { break; } currScope = currScope.parentScope; } return false; } function identInLocalScope(identName, state) { return state.localScope.identifiers[identName] !== undefined; } function declareIdentInLocalScope(identName, state) { state.localScope.identifiers[identName] = true; } /** * Apply the given analyzer function to the current node. If the analyzer * doesn't return false, traverse each child of the current node using the given * traverser function. * * @param {function} analyzer * @param {function} traverser * @param {object} node * @param {function} visitor * @param {array} path * @param {object} state */ function analyzeAndTraverse(analyzer, traverser, node, path, state) { var key, child; if (node.type) { if (analyzer(node, path, state) === false) { return; } path.unshift(node); } for (key in node) { // skip obviously wrong attributes if (key === 'range' || key === 'loc') { continue; } if (node.hasOwnProperty(key)) { child = node[key]; if (typeof child === 'object' && child !== null) { traverser(child, path, state); } } } node.type && path.shift(); } /** * Checks whether a node or any of its sub-nodes contains * a syntactic construct of the passed type. * @param {object} node - AST node to test. * @param {string} type - node type to lookup. */ function containsChildOfType(node, type) { var foundMatchingChild = false; function nodeTypeAnalyzer(node) { if (node.type === type) { foundMatchingChild = true; return false; } } function nodeTypeTraverser(child, path, state) { if (!foundMatchingChild) { foundMatchingChild = containsChildOfType(child, type); } } analyzeAndTraverse( nodeTypeAnalyzer, nodeTypeTraverser, node, [] ); return foundMatchingChild; } exports.append = append; exports.catchup = catchup; exports.catchupWhiteSpace = catchupWhiteSpace; exports.catchupNewlines = catchupNewlines; exports.containsChildOfType = containsChildOfType; exports.createState = createState; exports.declareIdentInLocalScope = declareIdentInLocalScope; exports.getDocblock = getDocblock; exports.identWithinLexicalScope = identWithinLexicalScope; exports.identInLocalScope = identInLocalScope; exports.indentBefore = indentBefore; exports.move = move; exports.updateIndent = updateIndent; exports.updateState = updateState; exports.analyzeAndTraverse = analyzeAndTraverse;