UNPKG

7.22 kBJavaScriptView Raw
1/**
2 * @fileoverview functions for scanning an AST for globals including
3 * traversing referenced scripts.
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 */
8
9"use strict";
10
11const path = require("path");
12const fs = require("fs");
13const helpers = require("./helpers");
14const eslintScope = require("eslint-scope");
15
16/**
17 * Parses a list of "name:boolean_value" or/and "name" options divided by comma
18 * or whitespace.
19 *
20 * This function was copied from eslint.js
21 *
22 * @param {string} string The string to parse.
23 * @param {Comment} comment The comment node which has the string.
24 * @returns {Object} Result map object of names and boolean values
25 */
26function parseBooleanConfig(string, comment) {
27 let items = {};
28
29 // Collapse whitespace around : to make parsing easier
30 string = string.replace(/\s*:\s*/g, ":");
31 // Collapse whitespace around ,
32 string = string.replace(/\s*,\s*/g, ",");
33
34 string.split(/\s|,+/).forEach(function(name) {
35 if (!name) {
36 return;
37 }
38
39 let pos = name.indexOf(":");
40 let value;
41 if (pos !== -1) {
42 value = name.substring(pos + 1, name.length);
43 name = name.substring(0, pos);
44 }
45
46 items[name] = {
47 value: (value === "true"),
48 comment
49 };
50 });
51
52 return items;
53}
54
55/**
56 * Global discovery can require parsing many files. This map of
57 * {String} => {Object} caches what globals were discovered for a file path.
58 */
59const globalCache = new Map();
60
61/**
62 * Global discovery can occasionally meet circular dependencies due to the way
63 * js files are included via xul files etc. This set is used to avoid getting
64 * into loops whilst the discovery is in progress.
65 */
66var globalDiscoveryInProgressForFiles = new Set();
67
68/**
69 * An object that returns found globals for given AST node types. Each prototype
70 * property should be named for a node type and accepts a node parameter and a
71 * parents parameter which is a list of the parent nodes of the current node.
72 * Each returns an array of globals found.
73 *
74 * @param {String} filePath
75 * The absolute path of the file being parsed.
76 */
77function GlobalsForNode(filePath) {
78 this.path = filePath;
79 this.dirname = path.dirname(this.path);
80}
81
82GlobalsForNode.prototype = {
83 Program(node) {
84 let globals = [];
85 for (let comment of node.comments) {
86 if (comment.type !== "Block") {
87 continue;
88 }
89 let value = comment.value.trim();
90 value = value.replace(/\n/g, "");
91
92 // We have to discover any globals that ESLint would have defined through
93 // comment directives.
94 let match = /^globals?\s+(.+)/.exec(value);
95 if (match) {
96 let values = parseBooleanConfig(match[1].trim(), node);
97 for (let name of Object.keys(values)) {
98 globals.push({
99 name,
100 writable: values[name].value
101 });
102 }
103 // We matched globals, so we won't match import-globals-from.
104 continue;
105 }
106
107 match = /^import-globals-from\s+(.+)$/.exec(value);
108 if (!match) {
109 continue;
110 }
111
112 let filePath = match[1].trim();
113
114 if (!path.isAbsolute(filePath)) {
115 filePath = path.resolve(this.dirname, filePath);
116 }
117 globals = globals.concat(module.exports.getGlobalsForFile(filePath));
118 }
119
120 return globals;
121 },
122
123 ExpressionStatement(node, parents, globalScope) {
124 let isGlobal = helpers.getIsGlobalScope(parents);
125 let globals = [];
126
127 // Note: We check the expression types here and only call the necessary
128 // functions to aid performance.
129 if (node.expression.type === "AssignmentExpression") {
130 globals = helpers.convertThisAssignmentExpressionToGlobals(node, isGlobal);
131 } else if (node.expression.type === "CallExpression") {
132 globals = helpers.convertCallExpressionToGlobals(node, isGlobal);
133 }
134
135 // Here we assume that if importScripts is set in the global scope, then
136 // this is a worker. It would be nice if eslint gave us a way of getting
137 // the environment directly.
138 if (globalScope && globalScope.set.get("importScripts")) {
139 let workerDetails = helpers.convertWorkerExpressionToGlobals(node,
140 isGlobal, this.dirname);
141 globals = globals.concat(workerDetails);
142 }
143
144 return globals;
145 }
146};
147
148module.exports = {
149 /**
150 * Returns all globals for a given file. Recursively searches through
151 * import-globals-from directives and also includes globals defined by
152 * standard eslint directives.
153 *
154 * @param {String} filePath
155 * The absolute path of the file to be parsed.
156 * @return {Array}
157 * An array of objects that contain details about the globals:
158 * - {String} name
159 * The name of the global.
160 * - {Boolean} writable
161 * If the global is writeable or not.
162 */
163 getGlobalsForFile(filePath) {
164 if (globalCache.has(filePath)) {
165 return globalCache.get(filePath);
166 }
167
168 if (globalDiscoveryInProgressForFiles.has(filePath)) {
169 // We're already processing this file, so return an empty set for now -
170 // the initial processing will pick up on the globals for this file.
171 return [];
172 }
173 globalDiscoveryInProgressForFiles.add(filePath);
174
175 let content = fs.readFileSync(filePath, "utf8");
176
177 // Parse the content into an AST
178 let ast = helpers.getAST(content);
179
180 // Discover global declarations
181 // The second parameter works around https://github.com/babel/babel-eslint/issues/470
182 let scopeManager = eslintScope.analyze(ast, {});
183 let globalScope = scopeManager.acquire(ast);
184
185 let globals = Object.keys(globalScope.variables).map(v => ({
186 name: globalScope.variables[v].name,
187 writable: true
188 }));
189
190 // Walk over the AST to find any of our custom globals
191 let handler = new GlobalsForNode(filePath);
192
193 helpers.walkAST(ast, (type, node, parents) => {
194 if (type in handler) {
195 let newGlobals = handler[type](node, parents, globalScope);
196 globals.push.apply(globals, newGlobals);
197 }
198 });
199
200 globalCache.set(filePath, globals);
201
202 globalDiscoveryInProgressForFiles.delete(filePath);
203 return globals;
204 },
205
206 /**
207 * Intended to be used as-is for an ESLint rule that parses for globals in
208 * the current file and recurses through import-globals-from directives.
209 *
210 * @param {Object} context
211 * The ESLint parsing context.
212 */
213 getESLintGlobalParser(context) {
214 let globalScope;
215
216 let parser = {
217 Program(node) {
218 globalScope = context.getScope();
219 }
220 };
221
222 // Install thin wrappers around GlobalsForNode
223 let handler = new GlobalsForNode(helpers.getAbsoluteFilePath(context));
224
225 for (let type of Object.keys(GlobalsForNode.prototype)) {
226 parser[type] = function(node) {
227 if (type === "Program") {
228 globalScope = context.getScope();
229 }
230 let globals = handler[type](node, context.getAncestors(), globalScope);
231 helpers.addGlobals(globals, globalScope);
232 };
233 }
234
235 return parser;
236 }
237};