/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/extensions/default/JavaScriptRefactoring/RefactoringUtils.js
585 строк
21 KB
Nitesh Kumar
JSRefactoring Bugs Fix (#14455)
02 авг 2018, 12:50
02 авг 2018, 12:50
afcbff0
Код
Авторство
О чём код?
/* * Copyright (c) 2013 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /* * Utilities functions related to refactoring */ define(function (require, exports, module) { 'use strict'; var Acorn = brackets.getModule("thirdparty/acorn/dist/acorn"), ASTWalker = brackets.getModule("thirdparty/acorn/dist/walk"), MessageIds = brackets.getModule("JSUtils/MessageIds"), _ = brackets.getModule("thirdparty/lodash"), AcornLoose = brackets.getModule("thirdparty/acorn/dist/acorn_loose"), ScopeManager = brackets.getModule("JSUtils/ScopeManager"); var templates = JSON.parse(require("text!Templates.json")); // Length of the function body used as function name for nameless functions var FUNCTION_BODY_PREFIX_LENGTH = 30; /** * Checks whether two ast nodes are equal * @param {!ASTNode} a * @param {!ASTNode} b * @return {boolean} */ function isEqual(a, b) { return a.start === b.start && a.end === b.end; } /** * Gets a expression surrounding start and end (if any) * @param {!ASTNode} ast - the ast of the complete file * @param {!number} start - the start offset * @param {!number} end - the end offset * @param {!string} fileText - the entire file text * @return {ASTNode|boolean} */ function getExpression(ast, start, end, fileText) { var expn = findSurroundExpression(ast, {start: start, end: end}); if (!expn) { return false; } // Class Expression also includes the trailing semicolon // Add special case for it if (expn.type === "ClassExpression" && expn.start === start && expn.end - end <= 1) { expn.end = end; return expn; } else if (expn.start === start && expn.end === end) { return expn; } // Subexpressions are possible only for BinaryExpression, LogicalExpression and SequenceExpression if (!(["BinaryExpression", "LogicalExpression", "SequenceExpression"].includes(expn.type))) { return false; } // Check subexpression var parentExpn = expn; var parentExpStr = fileText.substr(parentExpn.start, parentExpn.end - parentExpn.start); // Check whether the parentExpn forms a valid expression after replacing the sub expression var str = parentExpStr.substr(0, start - parentExpn.start) + "placeHolder" + parentExpStr.substr(end - parentExpn.start); var node = isStandAloneExpression(str); if (node && node.type === parentExpn.type) { return parentExpn; } return false; } function getAST(text) { var ast; try { ast = Acorn.parse(text, {ecmaVersion: 9}); } catch(e) { ast = Acorn.parse_dammit(text, {ecmaVersion: 9}); } return ast; } /* * Checks whether the text between start and end offsets form a valid set of statements * @param {!ASTNode} ast - the ast of the complete file * @param {!number} start - the start offset * @param {!number} end - the end offset * @param {!string} fileText - the entire file text * @return {boolean} */ function checkStatement(ast, start, end, fileText) { // Do not allow function or class nodes var notStatement = false; ASTWalker.simple(getAST(fileText.substr(start, end - start)), { FunctionDeclaration: function (node) { notStatement = true; }, ClassDeclaration: function (node) { notStatement = true; } }); if (notStatement) { return false; } var startStatement = findSurroundASTNode(ast, {start: start}, ["Statement"]); var endStatement = findSurroundASTNode(ast, {start: end}, ["Statement"]); return startStatement && endStatement && startStatement.start === start && startStatement.end <= end && endStatement.start >= start && endStatement.end === end; } /** * Gets a unique identifier name in the scope that starts with prefix * @param {!Scope} scopes - an array of all scopes returned from tern (each element contains 'props' with identifiers * in that scope) * @param {!string} prefix - prefix of the identifier * @param {number} num - number to start checking for * @return {!string} identifier name */ function getUniqueIdentifierName(scopes, prefix, num) { if (!scopes) { return prefix; } var props = scopes.reduce(function(props, scope) { return _.union(props, _.keys(scope.props)); }, []); if (!props) { return prefix; } num = num || "1"; var name; while (num < 100) { // limit search length name = prefix + num; if (props.indexOf(name) === -1) { break; } ++num; } return name; } /** * Returns the no of lines in the text * @param {!string} text * @return {number} */ function numLines(text) { return text.split("\n").length; } /** * Checks whether the text forms a stand alone expression without considering the context of text * @param {!string} text * @return {boolean} */ function isStandAloneExpression(text) { var found = ASTWalker.findNodeAt(getAST(text), 0, text.length, function (nodeType, node) { if (nodeType === "Expression") { return true; } return false; }); return found && found.node; } /** * Requests scope data from tern * @param {!Session} session * @param {!{line: number, ch: number}} offset * @return {!$.Promise} a jQuery promise that will be resolved with the scope data */ function getScopeData(session, offset) { var path = session.path, fileInfo = { type: MessageIds.TERN_FILE_INFO_TYPE_FULL, name: path, offsetLines: 0, text: ScopeManager.filterText(session.getJavascriptText()) }; ScopeManager.postMessage({ type: MessageIds.TERN_SCOPEDATA_MSG, fileInfo: fileInfo, offset: offset }); var ternPromise = ScopeManager.addPendingRequest(fileInfo.name, offset, MessageIds.TERN_SCOPEDATA_MSG); var result = new $.Deferred(); ternPromise.done(function (response) { result.resolveWith(null, [response.scope]); }).fail(function () { result.reject(); }); return result; } /** * Normalize text by removing leading and trailing whitespace characters * and moves the start and end offset to reflect the new offset * @param {!string} text - selected text * @param {!number} start - the start offset of the text * @param {!number} end - the end offset of the text * @param {!boolean} removeTrailingSemiColons - removes trailing semicolons also if true * @return {!{text: string, start: number, end: number}} */ function normalizeText(text, start, end, removeTrailingSemiColons) { var trimmedText; // Remove leading spaces trimmedText = _.trimLeft(text); if (trimmedText.length < text.length) { start += (text.length - trimmedText.length); } text = trimmedText; // Remove trailing spaces trimmedText = _.trimRight(text); if (trimmedText.length < text.length) { end -= (text.length - trimmedText.length); } text = trimmedText; // Remove trailing semicolons if (removeTrailingSemiColons) { trimmedText = _.trimRight(text, ';'); if (trimmedText.length < text.length) { end -= (text.length - trimmedText.length); } } return { text: trimmedText, start: start, end: end }; } /** * Checks whether the scope is a function scope */ function isFnScope(scope) { return !scope.isBlock && !scope.isCatch; } function findSurroundExpression(ast, expn) { var start = expn.start; var end = expn.end; var surroundExpn; while (true) { surroundExpn = findSurroundASTNode(ast, {start: start, end: end}, ["Expression"]); if (!surroundExpn) { return null; } // Do not allow sequence expressions if (surroundExpn.type === "SequenceExpression") { start = surroundExpn.start - 1; } else if (surroundExpn.type === "FunctionExpression") { // Do not allow method definition expressions var methodDefinitionNode = findSurroundASTNode(ast, surroundExpn, ["MethodDefinition"]); if (methodDefinitionNode && isEqual(methodDefinitionNode.value, surroundExpn)) { start = surroundExpn.start - 1; } else { return surroundExpn; } } else { return surroundExpn; } } return surroundExpn; } /** * Finds the surrounding ast node of the given expression of any of the given types * @param {!ASTNode} ast * @param {!{start: number, end: number}} expn - contains start and end offsets of expn * @param {!Array.<string>} types * @return {?ASTNode} */ function findSurroundASTNode(ast, expn, types) { var foundNode = ASTWalker.findNodeAround(ast, expn.start, function (nodeType, node) { if (expn.end) { return types.includes(nodeType) && node.end >= expn.end; } else { return types.includes(nodeType); } }); return foundNode && _.clone(foundNode.node); } /** * Converts the scopes returned from tern to an array of scopes and adds id and name to the scope * Also checks for class scopes * @param {!ASTNode} ast - ast of the complete file * @param {!Scope} scope - scope returned from tern * @param {!string} fullText - the complete text of a file * @return {!Array.<Scope>} */ function getAllScopes(ast, scope, fullText) { var curScope = scope; var cnt = 0; var scopes = []; while (curScope) { curScope.id = cnt++; scopes.push(curScope); if (curScope.fnType) { // Check for class scopes surrounding the function if (curScope.fnType === "FunctionExpression") { var methodDefinitionNode = findSurroundASTNode(ast, curScope.originNode, ["MethodDefinition"]); // class scope found if (methodDefinitionNode && isEqual(methodDefinitionNode.value, curScope.originNode)) { // Change curScope name and originNode to that of methodDefinitionNode curScope.name = methodDefinitionNode.key.name; curScope.originNode = methodDefinitionNode; var classNode = findSurroundASTNode(ast, methodDefinitionNode, ["ClassDeclaration", "ClassExpression"]); if (classNode) { // Class Declaration found add it to scopes var temp = curScope.prev; var newScope = {}; newScope.isClass = true; // if the class is class expression, check if it has a name if (classNode.type === "ClassExpression") { var assignmentExpNode = findSurroundASTNode(ast, classNode, ["AssignmentExpression"]); if (assignmentExpNode && assignmentExpNode.left && assignmentExpNode.left.name) { newScope.name = "class " + assignmentExpNode.left.name; } else { var varDeclaratorNode = findSurroundASTNode(ast, classNode, ["VariableDeclarator"]); if (varDeclaratorNode && varDeclaratorNode.id && varDeclaratorNode.id.name) { newScope.name = "class " + varDeclaratorNode.id.name; } else { newScope.name = "class null"; } } } else { newScope.name = "class " + (classNode.id && classNode.id.name); } newScope.originNode = classNode; curScope.prev = newScope; newScope.prev = temp; } } else { // For function expressions, assign name to prefix of the function body curScope.name = "function starting with " + fullText.substr( curScope.originNode.body.start, Math.min( FUNCTION_BODY_PREFIX_LENGTH, curScope.originNode.body.end - curScope.originNode.body.start ) ); } } else { // Acorn parse_dammit marks name with '✖' under erroneous declarations, check it if (curScope.fnType === "✖") { curScope.name = "function starting with " + fullText.substr( curScope.originNode.body.start, Math.min( FUNCTION_BODY_PREFIX_LENGTH, curScope.originNode.body.end - curScope.originNode.body.start ) ); } else { curScope.name = curScope.fnType; } } } else if (!curScope.originNode) { curScope.name = "global"; } curScope = curScope.prev; } return scopes; } /** * Note - To use these state defined in Refactoring Session, * Please reinitialize this RefactoringSession after performing any of the below operations * (i.e. replaceRange, setSelection or indentLine) * * RefactoringSession objects encapsulate state associated with a refactoring session * and This will help finding information around documents, selection, * position, ast, and queries around AST nodes * * @constructor * @param {Editor} editor - the editor context for the session */ function RefactoringSession(editor) { this.editor = editor; this.document = editor.document; this.selection = editor.getSelection(); this.text = this.document.getText(); this.selectedText = editor.getSelectedText(); this.cm = editor._codeMirror; this.startIndex = editor.indexFromPos(this.selection.start); this.endIndex = editor.indexFromPos(this.selection.end); this.startPos = this.selection.start; this.endPos = this.selection.end; this.ast = this.createAstOfCurrentDoc(); } /** * Get the end position of given line * * @param {number} line - line number * @return {{line: number, ch: number}} - line end position */ RefactoringSession.prototype.lineEndPosition = function (line) { var lineText = this.document.getLine(line); return { line: line, ch: lineText.length }; }; /** * Get the ast of current opened document in focused editor * * @return {Object} - Ast of current opened doc */ RefactoringSession.prototype.createAstOfCurrentDoc = function () { var ast, text = this.document.getText(); try { ast = Acorn.parse(text); } catch(e) { ast = Acorn.parse_dammit(text); } return ast; }; /** * This will add template at given position/selection * * @param {string} template - name of the template defined in templates.json * @param {Array} args- Check all arguments that exist in defined templated pass all that args as array * @param {{line: number, ch: number}} rangeToReplace - Range which we want to replace * @param {string} subTemplate - If template written under some category */ RefactoringSession.prototype.replaceTextFromTemplate = function (template, args, rangeToReplace, subTemplate) { var templateText = templates[template]; if (subTemplate) { templateText = templateText[subTemplate]; } var compiled = _.template(templateText), formattedText = compiled(args); if (!rangeToReplace) { rangeToReplace = this.editor.getSelection(); } this.document.replaceRange(formattedText, rangeToReplace.start, rangeToReplace.end); var startLine = rangeToReplace.start.line, endLine = startLine + formattedText.split("\n").length; for (var i = startLine + 1; i < endLine; i++) { this.cm.indentLine(i); } }; /** * Get Params of selected function * * @param {number} start- start offset * @param {number} end - end offset * @param {string} selectedText - Create ast for only selected node * @return {Array} param - Array of all parameters in function */ RefactoringSession.prototype.getParamsOfFunction = function getParamsOfFunction(start, end, selectedText) { var param = []; ASTWalker.simple(AcornLoose.parse_dammit(selectedText), { Function: function (node) { if (node.type === "FunctionDeclaration") { node.params.forEach(function (item) { param.push(item.name); }); } } }); return param; }; /** * Get the Parent node * * @param {Object} ast - ast of full document * @param {number} start - start Offset * @return {Object} node - Returns the parent node of node which is at offset start */ RefactoringSession.prototype.getParentNode = function (ast, start) { var foundNode = ASTWalker.findNodeAround(ast, start, function(nodeType, node) { return (nodeType === "ObjectExpression"); }); return foundNode && foundNode.node; }; /** * Checks weather the node at start is last in that scope or not * * @param {Object} ast - ast of full document * @param {number} start - start Offset * @return {boolean} - is last node in that scope */ RefactoringSession.prototype.isLastNodeInScope = function (ast, start) { var parentNode = this.getParentNode(ast, start), currentNodeStart; ASTWalker.simple(parentNode, { Property: function (node) { currentNodeStart = node.start; } }); return start >= currentNodeStart; }; // Define public api exports.isEqual = isEqual; exports.getUniqueIdentifierName = getUniqueIdentifierName; exports.isStandAloneExpression = isStandAloneExpression; exports.numLines = numLines; exports.getScopeData = getScopeData; exports.normalizeText = normalizeText; exports.getExpression = getExpression; exports.isFnScope = isFnScope; exports.getAllScopes = getAllScopes; exports.checkStatement = checkStatement; exports.findSurroundASTNode = findSurroundASTNode; exports.getAST = getAST; exports.findSurroundExpression = findSurroundExpression; exports.RefactoringSession = RefactoringSession; });