globals.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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. "use strict";
  9. const path = require("path");
  10. const fs = require("fs");
  11. const helpers = require("./helpers");
  12. const escope = require("escope");
  13. const estraverse = require("estraverse");
  14. /**
  15. * Parses a list of "name:boolean_value" or/and "name" options divided by comma or
  16. * whitespace.
  17. *
  18. * This function was copied from eslint.js
  19. *
  20. * @param {string} string The string to parse.
  21. * @param {Comment} comment The comment node which has the string.
  22. * @returns {Object} Result map object of names and boolean values
  23. */
  24. function parseBooleanConfig(string, comment) {
  25. let items = {};
  26. // Collapse whitespace around : to make parsing easier
  27. string = string.replace(/\s*:\s*/g, ":");
  28. // Collapse whitespace around ,
  29. string = string.replace(/\s*,\s*/g, ",");
  30. string.split(/\s|,+/).forEach(function(name) {
  31. if (!name) {
  32. return;
  33. }
  34. let pos = name.indexOf(":");
  35. let value = undefined;
  36. if (pos !== -1) {
  37. value = name.substring(pos + 1, name.length);
  38. name = name.substring(0, pos);
  39. }
  40. items[name] = {
  41. value: (value === "true"),
  42. comment: comment
  43. };
  44. });
  45. return items;
  46. }
  47. /**
  48. * Global discovery can require parsing many files. This map of
  49. * {String} => {Object} caches what globals were discovered for a file path.
  50. */
  51. const globalCache = new Map();
  52. /**
  53. * An object that returns found globals for given AST node types. Each prototype
  54. * property should be named for a node type and accepts a node parameter and a
  55. * parents parameter which is a list of the parent nodes of the current node.
  56. * Each returns an array of globals found.
  57. *
  58. * @param {String} path
  59. * The absolute path of the file being parsed.
  60. */
  61. function GlobalsForNode(path) {
  62. this.path = path;
  63. this.root = helpers.getRootDir(path);
  64. }
  65. GlobalsForNode.prototype = {
  66. BlockComment(node, parents) {
  67. let value = node.value.trim();
  68. let match = /^import-globals-from\s+(.+)$/.exec(value);
  69. if (!match) {
  70. return [];
  71. }
  72. let filePath = match[1].trim();
  73. if (!path.isAbsolute(filePath)) {
  74. let dirName = path.dirname(this.path);
  75. filePath = path.resolve(dirName, filePath);
  76. }
  77. return module.exports.getGlobalsForFile(filePath);
  78. },
  79. ExpressionStatement(node, parents) {
  80. let isGlobal = helpers.getIsGlobalScope(parents);
  81. let names = helpers.convertExpressionToGlobals(node, isGlobal, this.root);
  82. return names.map(name => { return { name, writable: true }});
  83. },
  84. };
  85. module.exports = {
  86. /**
  87. * Returns all globals for a given file. Recursively searches through
  88. * import-globals-from directives and also includes globals defined by
  89. * standard eslint directives.
  90. *
  91. * @param {String} path
  92. * The absolute path of the file to be parsed.
  93. */
  94. getGlobalsForFile(path) {
  95. if (globalCache.has(path)) {
  96. return globalCache.get(path);
  97. }
  98. let content = fs.readFileSync(path, "utf8");
  99. // Parse the content into an AST
  100. let ast = helpers.getAST(content);
  101. // Discover global declarations
  102. let scopeManager = escope.analyze(ast);
  103. let globalScope = scopeManager.acquire(ast);
  104. let globals = Object.keys(globalScope.variables).map(v => ({
  105. name: globalScope.variables[v].name,
  106. writable: true,
  107. }));
  108. // Walk over the AST to find any of our custom globals
  109. let handler = new GlobalsForNode(path);
  110. helpers.walkAST(ast, (type, node, parents) => {
  111. // We have to discover any globals that ESLint would have defined through
  112. // comment directives
  113. if (type == "BlockComment") {
  114. let value = node.value.trim();
  115. let match = /^globals?\s+(.+)$/.exec(value);
  116. if (match) {
  117. let values = parseBooleanConfig(match[1].trim(), node);
  118. for (let name of Object.keys(values)) {
  119. globals.push({
  120. name,
  121. writable: values[name].value
  122. })
  123. }
  124. }
  125. }
  126. if (type in handler) {
  127. let newGlobals = handler[type](node, parents);
  128. globals.push.apply(globals, newGlobals);
  129. }
  130. });
  131. globalCache.set(path, globals);
  132. return globals;
  133. },
  134. /**
  135. * Intended to be used as-is for an ESLint rule that parses for globals in
  136. * the current file and recurses through import-globals-from directives.
  137. *
  138. * @param {Object} context
  139. * The ESLint parsing context.
  140. */
  141. getESLintGlobalParser(context) {
  142. let globalScope;
  143. let parser = {
  144. Program(node) {
  145. globalScope = context.getScope();
  146. }
  147. };
  148. // Install thin wrappers around GlobalsForNode
  149. let handler = new GlobalsForNode(helpers.getAbsoluteFilePath(context));
  150. for (let type of Object.keys(GlobalsForNode.prototype)) {
  151. parser[type] = function(node) {
  152. let globals = handler[type](node, context.getAncestors());
  153. helpers.addGlobals(globals, globalScope);
  154. }
  155. }
  156. return parser;
  157. }
  158. };