UNPKG

7.87 kBJavaScriptView Raw
1/**
2 * @fileoverview Abstraction of JavaScript source code.
3 * @author Nicholas C. Zakas
4 * @copyright 2015 Nicholas C. Zakas. All rights reserved.
5 * See LICENSE file in root directory for full license.
6 */
7"use strict";
8/* eslint no-underscore-dangle: 0*/
9
10//------------------------------------------------------------------------------
11// Requirements
12//------------------------------------------------------------------------------
13
14var createTokenStore = require("../token-store.js"),
15 estraverse = require("estraverse"),
16 assign = require("object-assign");
17
18//------------------------------------------------------------------------------
19// Private
20//------------------------------------------------------------------------------
21
22/**
23 * Validates that the given AST has the required information.
24 * @param {ASTNode} ast The Program node of the AST to check.
25 * @throws {Error} If the AST doesn't contain the correct information.
26 * @returns {void}
27 * @private
28 */
29function validate(ast) {
30
31 if (!ast.tokens) {
32 throw new Error("AST is missing the tokens array.");
33 }
34
35 if (!ast.comments) {
36 throw new Error("AST is missing the comments array.");
37 }
38
39 if (!ast.loc) {
40 throw new Error("AST is missing location information.");
41 }
42
43 if (!ast.range) {
44 throw new Error("AST is missing range information");
45 }
46}
47
48/**
49 * Finds a JSDoc comment node in an array of comment nodes.
50 * @param {ASTNode[]} comments The array of comment nodes to search.
51 * @param {int} line Line number to look around
52 * @returns {ASTNode} The node if found, null if not.
53 * @private
54 */
55function findJSDocComment(comments, line) {
56
57 if (comments) {
58 for (var i = comments.length - 1; i >= 0; i--) {
59 if (comments[i].type === "Block" && comments[i].value.charAt(0) === "*") {
60
61 if (line - comments[i].loc.end.line <= 1) {
62 return comments[i];
63 } else {
64 break;
65 }
66 }
67 }
68 }
69
70 return null;
71}
72
73/**
74 * Check to see if its a ES6 export declaration
75 * @param {ASTNode} astNode - any node
76 * @returns {boolean} whether the given node represents a export declaration
77 * @private
78 */
79function looksLikeExport(astNode) {
80 return astNode.type === "ExportDefaultDeclaration" || astNode.type === "ExportNamedDeclaration" ||
81 astNode.type === "ExportAllDeclaration" || astNode.type === "ExportSpecifier";
82}
83
84
85//------------------------------------------------------------------------------
86// Public Interface
87//------------------------------------------------------------------------------
88
89/**
90 * Represents parsed source code.
91 * @param {string} text The source code text.
92 * @param {ASTNode} ast The Program node of the AST representing the code.
93 * @constructor
94 */
95function SourceCode(text, ast) {
96
97 validate(ast);
98
99 /**
100 * The original text source code.
101 * @type string
102 */
103 this.text = text;
104
105 /**
106 * The parsed AST for the source code.
107 * @type ASTNode
108 */
109 this.ast = ast;
110
111 /**
112 * The source code split into lines according to ECMA-262 specification.
113 * This is done to avoid each rule needing to do so separately.
114 * @type string[]
115 */
116 this.lines = text.split(/\r\n|\r|\n|\u2028|\u2029/g);
117
118 /**
119 * Fixes to be made later on.
120 * @type Object[]
121 */
122 this._fixes = [];
123
124 // create token store methods
125 var tokenStore = createTokenStore(ast.tokens);
126 Object.keys(tokenStore).forEach(function(methodName) {
127 this[methodName] = tokenStore[methodName];
128 }, this);
129
130 // don't allow modification of this object
131 Object.freeze(this);
132 Object.freeze(this.lines);
133}
134
135SourceCode.prototype = {
136 constructor: SourceCode,
137
138 /**
139 * Gets the source code for the given node.
140 * @param {ASTNode=} node The AST node to get the text for.
141 * @param {int=} beforeCount The number of characters before the node to retrieve.
142 * @param {int=} afterCount The number of characters after the node to retrieve.
143 * @returns {string} The text representing the AST node.
144 */
145 getText: function(node, beforeCount, afterCount) {
146 if (node) {
147 return (this.text !== null) ? this.text.slice(Math.max(node.range[0] - (beforeCount || 0), 0),
148 node.range[1] + (afterCount || 0)) : null;
149 } else {
150 return this.text;
151 }
152
153 },
154
155 /**
156 * Gets the entire source text split into an array of lines.
157 * @returns {Array} The source text as an array of lines.
158 */
159 getLines: function() {
160 return this.lines;
161 },
162
163 /**
164 * Retrieves an array containing all comments in the source code.
165 * @returns {ASTNode[]} An array of comment nodes.
166 */
167 getAllComments: function() {
168 return this.ast.comments;
169 },
170
171 /**
172 * Gets all comments for the given node.
173 * @param {ASTNode} node The AST node to get the comments for.
174 * @returns {Object} The list of comments indexed by their position.
175 * @public
176 */
177 getComments: function(node) {
178
179 var leadingComments = node.leadingComments || [],
180 trailingComments = node.trailingComments || [];
181
182 /*
183 * espree adds a "comments" array on Program nodes rather than
184 * leadingComments/trailingComments. Comments are only left in the
185 * Program node comments array if there is no executable code.
186 */
187 if (node.type === "Program") {
188 if (node.body.length === 0) {
189 leadingComments = node.comments;
190 }
191 }
192
193 return {
194 leading: leadingComments,
195 trailing: trailingComments
196 };
197 },
198
199 /**
200 * Retrieves the JSDoc comment for a given node.
201 * @param {ASTNode} node The AST node to get the comment for.
202 * @returns {ASTNode} The BlockComment node containing the JSDoc for the
203 * given node or null if not found.
204 * @public
205 */
206 getJSDocComment: function(node) {
207
208 var parent = node.parent,
209 line = node.loc.start.line;
210
211 switch (node.type) {
212 case "FunctionDeclaration":
213 if (looksLikeExport(parent)) {
214 return findJSDocComment(parent.leadingComments, line);
215 } else {
216 return findJSDocComment(node.leadingComments, line);
217 }
218 break;
219
220 case "ArrowFunctionExpression":
221 case "FunctionExpression":
222
223 if (parent.type !== "CallExpression" && parent.type !== "NewExpression") {
224 while (parent && !parent.leadingComments && !/Function/.test(parent.type)) {
225 parent = parent.parent;
226 }
227
228 return parent && (parent.type !== "FunctionDeclaration") ? findJSDocComment(parent.leadingComments, line) : null;
229 }
230
231 // falls through
232
233 default:
234 return null;
235 }
236 },
237
238 /**
239 * Gets the deepest node containing a range index.
240 * @param {int} index Range index of the desired node.
241 * @returns {ASTNode} The node if found or null if not found.
242 */
243 getNodeByRangeIndex: function(index) {
244 var result = null;
245
246 estraverse.traverse(this.ast, {
247 enter: function(node, parent) {
248 if (node.range[0] <= index && index < node.range[1]) {
249 result = assign({ parent: parent }, node);
250 } else {
251 this.skip();
252 }
253 },
254 leave: function(node) {
255 if (node === result) {
256 this.break();
257 }
258 }
259 });
260
261 return result;
262 }
263
264};
265
266
267module.exports = SourceCode;