summaryrefslogtreecommitdiffstats
path: root/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
blob: acbfa06841174f3736c665774b301b70c37065fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/**
 * @fileoverview functions for scanning an AST for globals including
 *               traversing referenced scripts.
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

"use strict";

const path = require("path");
const fs = require("fs");
const helpers = require("./helpers");
const escope = require("escope");
const estraverse = require("estraverse");

/**
 * Parses a list of "name:boolean_value" or/and "name" options divided by comma or
 * whitespace.
 *
 * This function was copied from eslint.js
 *
 * @param {string} string The string to parse.
 * @param {Comment} comment The comment node which has the string.
 * @returns {Object} Result map object of names and boolean values
 */
function parseBooleanConfig(string, comment) {
  let items = {};

  // Collapse whitespace around : to make parsing easier
  string = string.replace(/\s*:\s*/g, ":");
  // Collapse whitespace around ,
  string = string.replace(/\s*,\s*/g, ",");

  string.split(/\s|,+/).forEach(function(name) {
    if (!name) {
      return;
    }

    let pos = name.indexOf(":");
    let value = undefined;
    if (pos !== -1) {
      value = name.substring(pos + 1, name.length);
      name = name.substring(0, pos);
    }

    items[name] = {
      value: (value === "true"),
      comment: comment
    };
  });

  return items;
}

/**
 * Global discovery can require parsing many files. This map of
 * {String} => {Object} caches what globals were discovered for a file path.
 */
const globalCache = new Map();

/**
 * An object that returns found globals for given AST node types. Each prototype
 * property should be named for a node type and accepts a node parameter and a
 * parents parameter which is a list of the parent nodes of the current node.
 * Each returns an array of globals found.
 *
 * @param  {String} path
 *         The absolute path of the file being parsed.
 */
function GlobalsForNode(path) {
  this.path = path;
  this.root = helpers.getRootDir(path);
}

GlobalsForNode.prototype = {
  BlockComment(node, parents) {
    let value = node.value.trim();
    let match = /^import-globals-from\s+(.+)$/.exec(value);
    if (!match) {
      return [];
    }

    let filePath = match[1].trim();

    if (!path.isAbsolute(filePath)) {
      let dirName = path.dirname(this.path);
      filePath = path.resolve(dirName, filePath);
    }

    return module.exports.getGlobalsForFile(filePath);
  },

  ExpressionStatement(node, parents) {
    let isGlobal = helpers.getIsGlobalScope(parents);
    let names = helpers.convertExpressionToGlobals(node, isGlobal, this.root);
    return names.map(name => { return { name, writable: true }});
  },
};

module.exports = {
  /**
   * Returns all globals for a given file. Recursively searches through
   * import-globals-from directives and also includes globals defined by
   * standard eslint directives.
   *
   * @param  {String} path
   *         The absolute path of the file to be parsed.
   */
  getGlobalsForFile(path) {
    if (globalCache.has(path)) {
      return globalCache.get(path);
    }

    let content = fs.readFileSync(path, "utf8");

    // Parse the content into an AST
    let ast = helpers.getAST(content);

    // Discover global declarations
    let scopeManager = escope.analyze(ast);
    let globalScope = scopeManager.acquire(ast);

    let globals = Object.keys(globalScope.variables).map(v => ({
      name: globalScope.variables[v].name,
      writable: true,
    }));

    // Walk over the AST to find any of our custom globals
    let handler = new GlobalsForNode(path);

    helpers.walkAST(ast, (type, node, parents) => {
      // We have to discover any globals that ESLint would have defined through
      // comment directives
      if (type == "BlockComment") {
        let value = node.value.trim();
        let match = /^globals?\s+(.+)$/.exec(value);
        if (match) {
          let values = parseBooleanConfig(match[1].trim(), node);
          for (let name of Object.keys(values)) {
            globals.push({
              name,
              writable: values[name].value
            })
          }
        }
      }

      if (type in handler) {
        let newGlobals = handler[type](node, parents);
        globals.push.apply(globals, newGlobals);
      }
    });

    globalCache.set(path, globals);

    return globals;
  },

  /**
   * Intended to be used as-is for an ESLint rule that parses for globals in
   * the current file and recurses through import-globals-from directives.
   *
   * @param  {Object} context
   *         The ESLint parsing context.
   */
  getESLintGlobalParser(context) {
    let globalScope;

    let parser = {
      Program(node) {
        globalScope = context.getScope();
      }
    };

    // Install thin wrappers around GlobalsForNode
    let handler = new GlobalsForNode(helpers.getAbsoluteFilePath(context));

    for (let type of Object.keys(GlobalsForNode.prototype)) {
      parser[type] = function(node) {
        let globals = handler[type](node, context.getAncestors());
        helpers.addGlobals(globals, globalScope);
      }
    }

    return parser;
  }
};