/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/language/JSUtils.js
521 строка
21 KB
Swagatam Mitra
JSUtils modification to handle es6 constructs (#13635)
28 авг 2017, 08:01
28 авг 2017, 08:01
9281909
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - 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. * */ /** * Set of utilities for simple parsing of JS text. */ define(function (require, exports, module) { "use strict"; var _ = require("thirdparty/lodash"), Acorn = require("thirdparty/acorn/dist/acorn"), AcornLoose = require("thirdparty/acorn/dist/acorn_loose"), ASTWalker = require("thirdparty/acorn/dist/walk"); // Load brackets modules var CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"), Async = require("utils/Async"), DocumentManager = require("document/DocumentManager"), ChangedDocumentTracker = require("document/ChangedDocumentTracker"), FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), PerfUtils = require("utils/PerfUtils"), StringUtils = require("utils/StringUtils"); /** * Tracks dirty documents between invocations of findMatchingFunctions. * @type {ChangedDocumentTracker} */ var _changedDocumentTracker = new ChangedDocumentTracker(); /** * @private * Return an object mapping function name to offset info for all functions in the specified text. * Offset info is an array, since multiple functions of the same name can exist. * @param {!string} text Document text * @return {Object.<string, Array.<{offsetStart: number, offsetEnd: number}>} */ function _findAllFunctionsInText(text) { var AST, results = {}, functionName, resultNode, memberPrefix, match; PerfUtils.markStart(PerfUtils.JSUTILS_REGEXP); try { AST = Acorn.parse(text, {locations: true}); } catch (e) { AST = AcornLoose.parse_dammit(text, {locations: true}); } function _addResult(node, offset, prefix) { memberPrefix = prefix ? prefix + " - " : ""; resultNode = node.id || node.key || node; functionName = resultNode.name; if (!Array.isArray(results[functionName])) { results[functionName] = []; } results[functionName].push( { offsetStart: offset || node.start, label: memberPrefix ? memberPrefix + functionName : null, location: resultNode.loc } ); } ASTWalker.simple(AST, { /* function <functionName> () {} */ FunctionDeclaration: function (node) { // As acorn_loose marks identifier names with '✖' under erroneous declarations // we should have a check to discard such 'FunctionDeclaration' nodes if (node.id.name !== '✖') { _addResult(node); } }, /* class <className> () {} */ ClassDeclaration: function (node) { _addResult(node); ASTWalker.simple(node, { /* class <className> () { <methodName> () { } } */ MethodDefinition: function (methodNode) { _addResult(methodNode, methodNode.key.start, node.id.name); } }); }, /* var <functionName> = function () {} or var <functionName> = () => {} */ VariableDeclarator: function (node) { if (node.init && (node.init.type === "FunctionExpression" || node.init.type === "ArrowFunctionExpression")) { _addResult(node); } }, /* SomeFunction.prototype.<functionName> = function () {} */ AssignmentExpression: function (node) { if (node.right && node.right.type === "FunctionExpression") { if (node.left && node.left.type === "MemberExpression" && node.left.property) { _addResult(node.left.property); } } }, /* { <functionName>: function() {} } */ Property: function (node) { if (node.value && node.value.type === "FunctionExpression") { if (node.key && node.key.type === "Identifier") { _addResult(node.key); } } }, /* <functionName>: function() {} */ LabeledStatement: function (node) { if (node.body && node.body.type === "FunctionDeclaration") { if (node.label) { _addResult(node.label); } } } }); PerfUtils.addMeasurement(PerfUtils.JSUTILS_REGEXP); return results; } // Given the start offset of a function definition (before the opening brace), find // the end offset for the function (the closing "}"). Returns the position one past the // close brace. Properly ignores braces inside comments, strings, and regexp literals. function _getFunctionEndOffset(text, offsetStart) { var mode = CodeMirror.getMode({}, "javascript"); var state = CodeMirror.startState(mode), stream, style, token; var curOffset = offsetStart, length = text.length, blockCount = 0, lineStart; var foundStartBrace = false; // Get a stream for the next line, and update curOffset and lineStart to point to the // beginning of that next line. Returns false if we're at the end of the text. function nextLine() { if (stream) { curOffset++; // account for \n if (curOffset >= length) { return false; } } lineStart = curOffset; var lineEnd = text.indexOf("\n", lineStart); if (lineEnd === -1) { lineEnd = length; } stream = new CodeMirror.StringStream(text.slice(curOffset, lineEnd)); return true; } // Get the next token, updating the style and token to refer to the current // token, and updating the curOffset to point to the end of the token (relative // to the start of the original text). function nextToken() { if (curOffset >= length) { return false; } if (stream) { // Set the start of the next token to the current stream position. stream.start = stream.pos; } while (!stream || stream.eol()) { if (!nextLine()) { return false; } } style = mode.token(stream, state); token = stream.current(); curOffset = lineStart + stream.pos; return true; } while (nextToken()) { if (style !== "comment" && style !== "regexp" && style !== "string" && style !== "string-2") { if (token === "{") { foundStartBrace = true; blockCount++; } else if (token === "}") { blockCount--; } } // blockCount starts at 0, so we don't want to check if it hits 0 // again until we've actually gone past the start of the function body. if (foundStartBrace && blockCount <= 0) { return curOffset; } } // Shouldn't get here, but if we do, return the end of the text as the offset. return length; } /** * @private * Computes function offsetEnd, lineStart and lineEnd. Appends a result record to rangeResults. * @param {!Document} doc * @param {!string} functionName * @param {!Array.<{offsetStart: number, offsetEnd: number}>} functions * @param {!Array.<{document: Document, name: string, lineStart: number, lineEnd: number}>} rangeResults */ function _computeOffsets(doc, functionName, functions, rangeResults) { var text = doc.getText(), lines = StringUtils.getLines(text); functions.forEach(function (funcEntry) { if (!funcEntry.offsetEnd) { PerfUtils.markStart(PerfUtils.JSUTILS_END_OFFSET); funcEntry.offsetEnd = _getFunctionEndOffset(text, funcEntry.offsetStart); funcEntry.lineStart = StringUtils.offsetToLineNum(lines, funcEntry.offsetStart); funcEntry.lineEnd = StringUtils.offsetToLineNum(lines, funcEntry.offsetEnd); PerfUtils.addMeasurement(PerfUtils.JSUTILS_END_OFFSET); } rangeResults.push({ document: doc, name: functionName, lineStart: funcEntry.lineStart, lineEnd: funcEntry.lineEnd }); }); } /** * @private * Read a file and build a function list. Result is cached in fileInfo. * @param {!FileInfo} fileInfo File to parse * @param {!$.Deferred} result Deferred to resolve with all functions found and the document */ function _readFile(fileInfo, result) { DocumentManager.getDocumentForPath(fileInfo.fullPath) .done(function (doc) { var allFunctions = _findAllFunctionsInText(doc.getText()); // Cache the result in the fileInfo object fileInfo.JSUtils = {}; fileInfo.JSUtils.functions = allFunctions; fileInfo.JSUtils.timestamp = doc.diskTimestamp; result.resolve({doc: doc, functions: allFunctions}); }) .fail(function (error) { result.reject(error); }); } /** * Determines if the document function cache is up to date. * @param {FileInfo} fileInfo * @return {$.Promise} A promise resolved with true with true when a function cache is available for the document. Resolves * with false when there is no cache or the cache is stale. */ function _shouldGetFromCache(fileInfo) { var result = new $.Deferred(), isChanged = _changedDocumentTracker.isPathChanged(fileInfo.fullPath); if (isChanged && fileInfo.JSUtils) { // See if it's dirty and in the working set first var doc = DocumentManager.getOpenDocumentForPath(fileInfo.fullPath); if (doc && doc.isDirty) { result.resolve(false); } else { // If a cache exists, check the timestamp on disk var file = FileSystem.getFileForPath(fileInfo.fullPath); file.stat(function (err, stat) { if (!err) { result.resolve(fileInfo.JSUtils.timestamp.getTime() === stat.mtime.getTime()); } else { result.reject(err); } }); } } else { // Use the cache if the file did not change and the cache exists result.resolve(!isChanged && fileInfo.JSUtils); } return result.promise(); } /** * @private * Compute lineStart and lineEnd for each matched function * @param {!Array.<{doc: Document, fileInfo: FileInfo, functions: Array.<offsetStart: number, offsetEnd: number>}>} docEntries * @param {!string} functionName * @param {!Array.<document: Document, name: string, lineStart: number, lineEnd: number>} rangeResults * @return {$.Promise} A promise resolved with an array of document ranges to populate a MultiRangeInlineEditor. */ function _getOffsetsForFunction(docEntries, functionName) { // Filter for documents that contain the named function var result = new $.Deferred(), matchedDocuments = [], rangeResults = []; docEntries.forEach(function (docEntry) { // Need to call _.has here since docEntry.functions could have an // entry for "hasOwnProperty", which results in an error if trying // to invoke docEntry.functions.hasOwnProperty(). if (_.has(docEntry.functions, functionName)) { var functionsInDocument = docEntry.functions[functionName]; matchedDocuments.push({doc: docEntry.doc, fileInfo: docEntry.fileInfo, functions: functionsInDocument}); } }); Async.doInParallel(matchedDocuments, function (docEntry) { var doc = docEntry.doc, oneResult = new $.Deferred(); // doc will be undefined if we hit the cache if (!doc) { DocumentManager.getDocumentForPath(docEntry.fileInfo.fullPath) .done(function (fetchedDoc) { _computeOffsets(fetchedDoc, functionName, docEntry.functions, rangeResults); }) .always(function () { oneResult.resolve(); }); } else { _computeOffsets(doc, functionName, docEntry.functions, rangeResults); oneResult.resolve(); } return oneResult.promise(); }).done(function () { result.resolve(rangeResults); }); return result.promise(); } /** * Resolves with a record containing the Document or FileInfo and an Array of all * function names with offsets for the specified file. Results may be cached. * @param {FileInfo} fileInfo * @return {$.Promise} A promise resolved with a document info object that * contains a map of all function names from the document and each function's start offset. */ function _getFunctionsForFile(fileInfo) { var result = new $.Deferred(); _shouldGetFromCache(fileInfo) .done(function (useCache) { if (useCache) { // Return cached data. doc property is undefined since we hit the cache. // _getOffsets() will fetch the Document if necessary. result.resolve({/*doc: undefined,*/fileInfo: fileInfo, functions: fileInfo.JSUtils.functions}); } else { _readFile(fileInfo, result); } }).fail(function (err) { result.reject(err); }); return result.promise(); } /** * @private * Get all functions for each FileInfo. * @param {Array.<FileInfo>} fileInfos * @return {$.Promise} A promise resolved with an array of document info objects that each * contain a map of all function names from the document and each function's start offset. */ function _getFunctionsInFiles(fileInfos) { var result = new $.Deferred(), docEntries = []; PerfUtils.markStart(PerfUtils.JSUTILS_GET_ALL_FUNCTIONS); Async.doInParallel(fileInfos, function (fileInfo) { var oneResult = new $.Deferred(); _getFunctionsForFile(fileInfo) .done(function (docInfo) { docEntries.push(docInfo); }) .always(function (error) { // If one file fails, continue to search oneResult.resolve(); }); return oneResult.promise(); }).always(function () { // Reset ChangedDocumentTracker now that the cache is up to date. _changedDocumentTracker.reset(); PerfUtils.addMeasurement(PerfUtils.JSUTILS_GET_ALL_FUNCTIONS); result.resolve(docEntries); }); return result.promise(); } /** * Return all functions that have the specified name, searching across all the given files. * * @param {!String} functionName The name to match. * @param {!Array.<File>} fileInfos The array of files to search. * @param {boolean=} keepAllFiles If true, don't ignore non-javascript files. * @return {$.Promise} that will be resolved with an Array of objects containing the * source document, start line, and end line (0-based, inclusive range) for each matching function list. * Does not addRef() the documents returned in the array. */ function findMatchingFunctions(functionName, fileInfos, keepAllFiles) { var result = new $.Deferred(), jsFiles = []; if (!keepAllFiles) { // Filter fileInfos for .js files jsFiles = fileInfos.filter(function (fileInfo) { return FileUtils.getFileExtension(fileInfo.fullPath).toLowerCase() === "js"; }); } else { jsFiles = fileInfos; } // RegExp search (or cache lookup) for all functions in the project _getFunctionsInFiles(jsFiles).done(function (docEntries) { // Compute offsets for all matched functions _getOffsetsForFunction(docEntries, functionName).done(function (rangeResults) { result.resolve(rangeResults); }); }); return result.promise(); } /** * Finds all instances of the specified searchName in "text". * Returns an Array of Objects with start and end properties. * * @param text {!String} JS text to search * @param searchName {!String} function name to search for * @return {Array.<{offset:number, functionName:string}>} * Array of objects containing the start offset for each matched function name. */ function findAllMatchingFunctionsInText(text, searchName) { var allFunctions = _findAllFunctionsInText(text); var result = []; var lines = text.split("\n"); _.forEach(allFunctions, function (functions, functionName) { if (functionName === searchName || searchName === "*") { functions.forEach(function (funcEntry) { var endOffset = _getFunctionEndOffset(text, funcEntry.offsetStart); result.push({ name: functionName, label: funcEntry.label, lineStart: StringUtils.offsetToLineNum(lines, funcEntry.offsetStart), lineEnd: StringUtils.offsetToLineNum(lines, endOffset), nameLineStart: funcEntry.location.start.line - 1, nameLineEnd: funcEntry.location.end.line - 1, columnStart: funcEntry.location.start.column, columnEnd: funcEntry.location.end.column }); }); } }); return result; } PerfUtils.createPerfMeasurement("JSUTILS_GET_ALL_FUNCTIONS", "Parallel file search across project"); PerfUtils.createPerfMeasurement("JSUTILS_REGEXP", "RegExp search for all functions"); PerfUtils.createPerfMeasurement("JSUTILS_END_OFFSET", "Find end offset for a single matched function"); exports.findAllMatchingFunctionsInText = findAllMatchingFunctionsInText; exports._getFunctionEndOffset = _getFunctionEndOffset; // For testing only exports.findMatchingFunctions = findMatchingFunctions; });